├── .gitignore ├── BVReorderTableView.h ├── BVReorderTableView.m ├── KEYPullDownMenu.h ├── KEYPullDownMenu.m ├── LICENSE ├── README.md ├── SKBounceAnimation.h ├── SKBounceAnimation.m └── demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | -------------------------------------------------------------------------------- /BVReorderTableView.h: -------------------------------------------------------------------------------- 1 | // 2 | // BVReorderTableView.h 3 | // 4 | // Created by Benjamin Vogelzang on 3/5/13. 5 | // Copyright (c) 2013 Ben Vogelzang. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | #import 26 | 27 | @protocol ReorderTableViewDelegate 28 | 29 | // This method is called when starting the re-ording process. You insert a blank row object into your 30 | // data source and return the object you want to save for later. This method is only called once. 31 | - (id)saveObjectAndInsertBlankRowAtIndexPath:(NSIndexPath *)indexPath; 32 | 33 | // This method is called when the selected row is dragged to a new position. You simply update your 34 | // data source to reflect that the rows have switched places. This can be called multiple times 35 | // during the reordering process 36 | - (void)moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath; 37 | 38 | // This method is called when the selected row is released to its new position. The object is the same 39 | // object you returned in saveObjectAndInsertBlankRowAtIndexPath:. Simply update the data source so the 40 | // object is in its new position. You should do any saving/cleanup here. 41 | - (void)finishReorderingWithObject:(id)object atIndexPath:(NSIndexPath *)indexPath; 42 | @end 43 | 44 | @interface BVReorderTableView : UITableView 45 | 46 | @property (nonatomic, assign) id delegate; 47 | @property (nonatomic, assign) CGFloat draggingRowHeight; 48 | @property (nonatomic, assign) BOOL canReorder; 49 | 50 | @end 51 | -------------------------------------------------------------------------------- /BVReorderTableView.m: -------------------------------------------------------------------------------- 1 | // 2 | // BVReorderTableView.m 3 | // 4 | // Copyright (c) 2013 Ben Vogelzang. 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | #import "BVReorderTableView.h" 25 | #import 26 | 27 | @interface BVReorderTableView () 28 | 29 | @property (nonatomic, strong) UILongPressGestureRecognizer *longPress; 30 | @property (nonatomic, strong) NSTimer *scrollingTimer; 31 | @property (nonatomic, assign) CGFloat scrollRate; 32 | @property (nonatomic, strong) NSIndexPath *currentLocationIndexPath; 33 | @property (nonatomic, strong) NSIndexPath *initialIndexPath; 34 | @property (nonatomic, strong) UIImageView *draggingView; 35 | @property (nonatomic, retain) id savedObject; 36 | 37 | - (void)initialize; 38 | - (void)longPress:(UILongPressGestureRecognizer *)gesture; 39 | - (void)updateCurrentLocation:(UILongPressGestureRecognizer *)gesture; 40 | - (void)scrollTableWithCell:(NSTimer *)timer; 41 | - (void)cancelGesture; 42 | 43 | @end 44 | 45 | 46 | 47 | @implementation BVReorderTableView 48 | 49 | @dynamic delegate, canReorder; 50 | @synthesize longPress; 51 | @synthesize scrollingTimer; 52 | @synthesize scrollRate; 53 | @synthesize currentLocationIndexPath; 54 | @synthesize draggingView; 55 | @synthesize savedObject; 56 | @synthesize draggingRowHeight; 57 | @synthesize initialIndexPath; 58 | 59 | - (id)init { 60 | return [self initWithFrame:CGRectZero]; 61 | } 62 | 63 | - (id)initWithFrame:(CGRect)frame { 64 | return [self initWithFrame:frame style:UITableViewStylePlain]; 65 | } 66 | 67 | - (id)initWithCoder:(NSCoder *)coder { 68 | self = [super initWithCoder:coder]; 69 | if (self) { 70 | [self initialize]; 71 | } 72 | return self; 73 | } 74 | 75 | - (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { 76 | self = [super initWithFrame:frame style:style]; 77 | if (self) { 78 | [self initialize]; 79 | } 80 | return self; 81 | } 82 | 83 | 84 | - (void)initialize { 85 | longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]; 86 | [self addGestureRecognizer:longPress]; 87 | 88 | self.canReorder = YES; 89 | } 90 | 91 | - (void)setCanReorder:(BOOL)canReorder { 92 | canReorder = canReorder; 93 | longPress.enabled = canReorder; 94 | } 95 | 96 | 97 | - (void)longPress:(UILongPressGestureRecognizer *)gesture { 98 | 99 | CGPoint location = [gesture locationInView:self]; 100 | NSIndexPath *indexPath = [self indexPathForRowAtPoint:location]; 101 | 102 | NSInteger sections = [self numberOfSections]; 103 | int rows = 0; 104 | for(int i = 0; i < sections; i++) { 105 | rows += [self numberOfRowsInSection:i]; 106 | } 107 | 108 | // get out of here if the long press was not on a valid row or our table is empty 109 | // or the dataSource tableView:canMoveRowAtIndexPath: doesn't allow moving the row 110 | if (rows == 0 || (gesture.state == UIGestureRecognizerStateBegan && indexPath == nil) || 111 | (gesture.state == UIGestureRecognizerStateEnded && self.currentLocationIndexPath == nil) || 112 | (gesture.state == UIGestureRecognizerStateBegan && 113 | [self.dataSource respondsToSelector:@selector(tableView:canMoveRowAtIndexPath:)] && 114 | indexPath && ![self.dataSource tableView:self canMoveRowAtIndexPath:indexPath])) { 115 | [self cancelGesture]; 116 | return; 117 | } 118 | 119 | // started 120 | if (gesture.state == UIGestureRecognizerStateBegan) { 121 | 122 | UITableViewCell *cell = [self cellForRowAtIndexPath:indexPath]; 123 | self.draggingRowHeight = cell.frame.size.height; 124 | [cell setHighlighted:NO animated:NO]; 125 | [cell setSelected:NO animated:NO]; 126 | 127 | 128 | // make an image from the pressed tableview cell 129 | UIGraphicsBeginImageContextWithOptions(cell.bounds.size, YES, 0); 130 | [cell.layer renderInContext:UIGraphicsGetCurrentContext()]; 131 | UIImage *cellImage = UIGraphicsGetImageFromCurrentImageContext(); 132 | UIGraphicsEndImageContext(); 133 | 134 | // create and image view that we will drag around the screen 135 | if (!draggingView) { 136 | draggingView = [[UIImageView alloc] initWithImage:cellImage]; 137 | [self addSubview:draggingView]; 138 | CGRect rect = [self rectForRowAtIndexPath:indexPath]; 139 | draggingView.frame = CGRectOffset(draggingView.bounds, rect.origin.x, rect.origin.y); 140 | [UIView animateWithDuration:0.3 animations:^{ 141 | 142 | draggingView.transform = CGAffineTransformMakeScale(1.03, 1.03); 143 | draggingView.center = CGPointMake(self.center.x, location.y); 144 | }]; 145 | } 146 | 147 | [self beginUpdates]; 148 | [self deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; 149 | [self insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; 150 | 151 | self.savedObject = [self.delegate saveObjectAndInsertBlankRowAtIndexPath:indexPath]; 152 | self.currentLocationIndexPath = indexPath; 153 | self.initialIndexPath = indexPath; 154 | [self endUpdates]; 155 | 156 | // enable scrolling for cell 157 | NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithObject:gesture forKey:@"gesture"]; 158 | self.scrollingTimer = [NSTimer timerWithTimeInterval:1/8 target:self selector:@selector(scrollTableWithCell:) userInfo:userInfo repeats:YES]; 159 | [[NSRunLoop mainRunLoop] addTimer:self.scrollingTimer forMode:NSDefaultRunLoopMode]; 160 | 161 | } 162 | // dragging 163 | else if (gesture.state == UIGestureRecognizerStateChanged) { 164 | // update position of the drag view 165 | // don't let it go past the top or the bottom too far 166 | if (location.y >= 0 && location.y <= self.contentSize.height + 50) { 167 | draggingView.center = CGPointMake(self.center.x, location.y); 168 | } 169 | 170 | CGRect rect = self.bounds; 171 | // adjust rect for content inset as we will use it below for calculating scroll zones 172 | rect.size.height -= self.contentInset.top; 173 | CGPoint location = [gesture locationInView:self]; 174 | 175 | [self updateCurrentLocation:gesture]; 176 | 177 | // tell us if we should scroll and which direction 178 | CGFloat scrollZoneHeight = rect.size.height / 6; 179 | CGFloat bottomScrollBeginning = self.contentOffset.y + self.contentInset.top + rect.size.height - scrollZoneHeight; 180 | CGFloat topScrollBeginning = self.contentOffset.y + self.contentInset.top + scrollZoneHeight; 181 | // we're in the bottom zone 182 | if (location.y >= bottomScrollBeginning) { 183 | self.scrollRate = (location.y - bottomScrollBeginning) / scrollZoneHeight; 184 | } 185 | // we're in the top zone 186 | else if (location.y <= topScrollBeginning) { 187 | self.scrollRate = (location.y - topScrollBeginning) / scrollZoneHeight; 188 | } 189 | else { 190 | self.scrollRate = 0; 191 | } 192 | } 193 | // dropped 194 | else if (gesture.state == UIGestureRecognizerStateEnded) { 195 | 196 | NSIndexPath *indexPath = self.currentLocationIndexPath; 197 | 198 | // remove scrolling timer 199 | [self.scrollingTimer invalidate]; 200 | self.scrollingTimer = nil; 201 | self.scrollRate = 0; 202 | 203 | // animate the drag view to the newly hovered cell 204 | [UIView animateWithDuration:0.1 205 | animations:^{ 206 | CGRect rect = [self rectForRowAtIndexPath:indexPath]; 207 | draggingView.transform = CGAffineTransformIdentity; 208 | draggingView.frame = CGRectOffset(draggingView.bounds, rect.origin.x, rect.origin.y); 209 | } completion:^(BOOL finished) { 210 | [draggingView removeFromSuperview]; 211 | 212 | [self beginUpdates]; 213 | [self deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; 214 | [self insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; 215 | [self.delegate finishReorderingWithObject:self.savedObject atIndexPath:indexPath]; 216 | [self endUpdates]; 217 | self.currentLocationIndexPath = nil; 218 | self.draggingView = nil; 219 | }]; 220 | } 221 | } 222 | 223 | 224 | - (void)updateCurrentLocation:(UILongPressGestureRecognizer *)gesture { 225 | 226 | NSIndexPath *indexPath = nil; 227 | CGPoint location = CGPointZero; 228 | 229 | // refresh index path 230 | location = [gesture locationInView:self]; 231 | indexPath = [self indexPathForRowAtPoint:location]; 232 | 233 | 234 | indexPath = [self.delegate tableView:self targetIndexPathForMoveFromRowAtIndexPath:self.initialIndexPath toProposedIndexPath:indexPath]; 235 | 236 | NSInteger oldHeight = [self rectForRowAtIndexPath:self.currentLocationIndexPath].size.height; 237 | NSInteger newHeight = [self rectForRowAtIndexPath:indexPath].size.height; 238 | 239 | if (indexPath && ![indexPath isEqual:self.currentLocationIndexPath] && [gesture locationInView:[self cellForRowAtIndexPath:indexPath]].y > newHeight - oldHeight) { 240 | [self beginUpdates]; 241 | [self deleteRowsAtIndexPaths:[NSArray arrayWithObject:self.currentLocationIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 242 | [self insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 243 | 244 | 245 | [self.delegate moveRowAtIndexPath:self.currentLocationIndexPath toIndexPath:indexPath]; 246 | 247 | self.currentLocationIndexPath = indexPath; 248 | [self endUpdates]; 249 | } 250 | } 251 | 252 | - (void)scrollTableWithCell:(NSTimer *)timer { 253 | 254 | UILongPressGestureRecognizer *gesture = [timer.userInfo objectForKey:@"gesture"]; 255 | CGPoint location = [gesture locationInView:self]; 256 | 257 | CGPoint currentOffset = self.contentOffset; 258 | CGPoint newOffset = CGPointMake(currentOffset.x, currentOffset.y + self.scrollRate); 259 | 260 | if (newOffset.y < -self.contentInset.top) { 261 | newOffset.y = -self.contentInset.top; 262 | } else if (self.contentSize.height < self.frame.size.height) { 263 | newOffset = currentOffset; 264 | } else if (newOffset.y > self.contentSize.height - self.frame.size.height) { 265 | newOffset.y = self.contentSize.height - self.frame.size.height; 266 | } else { 267 | } 268 | [self setContentOffset:newOffset]; 269 | 270 | if (location.y >= 0 && location.y <= self.contentSize.height + 50) { 271 | draggingView.center = CGPointMake(self.center.x, location.y); 272 | } 273 | 274 | [self updateCurrentLocation:gesture]; 275 | } 276 | 277 | - (void)cancelGesture { 278 | longPress.enabled = NO; 279 | longPress.enabled = YES; 280 | } 281 | 282 | @end -------------------------------------------------------------------------------- /KEYPullDownMenu.h: -------------------------------------------------------------------------------- 1 | // 2 | // KEYPullDownMenu.h 3 | // Keydown 4 | // 5 | // Created by mmackh on 10/11/13. 6 | // Copyright (c) 2013 Maximilian Mackh. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class KEYPullDownMenuItem; 12 | 13 | typedef void(^dismissBlock)(KEYPullDownMenuItem *item, NSInteger selectedRow); 14 | typedef void(^reorderBlock)(KEYPullDownMenuItem *item, NSInteger targetIndex); 15 | typedef void(^deleteBlock)(KEYPullDownMenuItem *item); 16 | 17 | @interface KEYPullDownMenu : UIView 18 | 19 | + (instancetype)openMenuInViewController:(UIViewController *)viewController items:(NSArray *)menuItems dismissBlock:dismissBlock reorderBlock:reorderBlock deleteBlock:deleteBlock; 20 | + (instancetype)dismissInViewController:(UIViewController *)viewController; 21 | 22 | @end 23 | 24 | @interface KEYPullDownMenuItem : NSObject 25 | 26 | + (instancetype)menuItemNamed:(NSString *)name deletable:(BOOL)deletable; 27 | 28 | @property (nonatomic,readwrite, getter = isActive) BOOL active; 29 | @property (nonatomic,readonly) NSString *name; 30 | @property (nonatomic,readonly) BOOL deletable; 31 | @property (nonatomic) NSMutableDictionary *dictionary; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /KEYPullDownMenu.m: -------------------------------------------------------------------------------- 1 | // 2 | // KEYPullDownMenu.m 3 | // Keydown 4 | // 5 | // Created by mmackh on 10/11/13. 6 | // Copyright (c) 2013 Maximilian Mackh. All rights reserved. 7 | // 8 | 9 | #import "KEYPullDownMenu.h" 10 | #import "SKBounceAnimation.h" 11 | #import "BVReorderTableView.h" 12 | 13 | #define kKEYPullDownAnimationDuration 0.3 14 | #define kKEYPullDownAnimationBounceHeight 20 15 | #define kKEYPullDownViewTag 198312 16 | 17 | @interface KEYPullDownMenuCell : UITableViewCell 18 | 19 | @end 20 | 21 | @interface KEYPullDownMenu () 22 | 23 | @property (nonatomic,strong) BVReorderTableView *tableView; 24 | @property (nonatomic,weak) UIViewController *parentViewController; 25 | @property (nonatomic,strong) NSMutableArray *items; 26 | @property (nonatomic,readonly) CGRect initialFrame; 27 | @property (nonatomic,readonly) CGRect finalFrame; 28 | 29 | @property (copy) dismissBlock dismissBlock; 30 | @property (copy) reorderBlock reorderBlock; 31 | @property (copy) deleteBlock deleteBlock; 32 | 33 | @end 34 | 35 | @interface KEYPullDownMenuItem () 36 | 37 | @property (nonatomic) BOOL dummy; 38 | 39 | @end 40 | 41 | @implementation KEYPullDownMenu 42 | 43 | - (id)init 44 | { 45 | self = [super init]; 46 | if (self) 47 | { 48 | self.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.7]; 49 | [self setAutoresizingMask:UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth]; 50 | 51 | self.tableView = [[BVReorderTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; 52 | [self.tableView registerClass:[KEYPullDownMenuCell class] forCellReuseIdentifier:@"Cell"]; 53 | [self.tableView setRowHeight:60]; 54 | [self.tableView setAutoresizingMask:(UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth)]; 55 | [self.tableView setBackgroundColor:[UIColor clearColor]]; 56 | [self.tableView setSeparatorColor:[UIColor clearColor]]; 57 | [self.tableView setDelegate:self]; 58 | [self.tableView setDataSource:self]; 59 | [self addSubview:self.tableView]; 60 | } 61 | return self; 62 | } 63 | 64 | #pragma mark - 65 | #pragma mark TableView 66 | 67 | - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath 68 | { 69 | [cell setBackgroundColor:[UIColor clearColor]]; 70 | [cell setAccessoryType:([self.items[indexPath.row] isActive])?UITableViewCellAccessoryCheckmark:UITableViewCellAccessoryNone]; 71 | 72 | } 73 | 74 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 75 | { 76 | return self.items.count; 77 | } 78 | 79 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 80 | { 81 | KEYPullDownMenuCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; 82 | cell.textLabel.text = [self.items[indexPath.row] name]; 83 | return cell; 84 | } 85 | 86 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath 87 | { 88 | if(self.items.count == 2) return NO; 89 | return [self.items[indexPath.row] deletable]; 90 | } 91 | 92 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 93 | if (editingStyle == UITableViewCellEditingStyleDelete) 94 | { 95 | KEYPullDownMenuItem *item = self.items[indexPath.row]; 96 | self.deleteBlock(item); 97 | [self.tableView beginUpdates]; 98 | [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 99 | [self.items removeObject:item]; 100 | [self.tableView endUpdates]; 101 | } 102 | } 103 | 104 | - (BOOL)tableView:(UITableView *)tableview canMoveRowAtIndexPath:(NSIndexPath *)indexPath 105 | { 106 | return [self.items[indexPath.row] deletable]; 107 | } 108 | 109 | - (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath 110 | { 111 | if (![self.items[sourceIndexPath.row] deletable] || ![self.items[proposedDestinationIndexPath.row] deletable]) return sourceIndexPath; 112 | return proposedDestinationIndexPath; 113 | } 114 | 115 | - (id)saveObjectAndInsertBlankRowAtIndexPath:(NSIndexPath *)indexPath 116 | { 117 | id object = [self.items objectAtIndex:indexPath.row]; 118 | KEYPullDownMenuItem *dummyItem = [KEYPullDownMenuItem menuItemNamed:@"" deletable:YES]; 119 | dummyItem.dummy = YES; 120 | [self.items replaceObjectAtIndex:indexPath.row withObject:dummyItem]; 121 | return object; 122 | } 123 | 124 | - (void)moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath 125 | { 126 | id object = [self.items objectAtIndex:fromIndexPath.row]; 127 | [self.items removeObjectAtIndex:fromIndexPath.row]; 128 | [self.items insertObject:object atIndex:toIndexPath.row]; 129 | } 130 | 131 | - (void)finishReorderingWithObject:(id)object atIndexPath:(NSIndexPath *)indexPath 132 | { 133 | [self.items replaceObjectAtIndex:indexPath.row withObject:object]; 134 | self.reorderBlock(object,indexPath.row); 135 | } 136 | 137 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 138 | { 139 | [self.tableView deselectRowAtIndexPath:indexPath animated:NO]; 140 | self.dismissBlock(self.items[indexPath.row],indexPath.row); 141 | } 142 | 143 | #pragma mark - 144 | #pragma mark Frames 145 | 146 | - (void)layoutSubviews 147 | { 148 | [super layoutSubviews]; 149 | if (self.frame.origin.y == self.initialFrame.origin.y) 150 | { 151 | self.frame = self.initialFrame; 152 | return; 153 | } 154 | self.frame = self.finalFrame; 155 | } 156 | 157 | - (CGRect)initialFrame 158 | { 159 | return CGRectMake(0, -self.parentViewController.view.frame.size.height, self.parentViewController.view.frame.size.width, self.parentViewController.view.frame.size.height); 160 | } 161 | 162 | - (CGRect)finalFrame 163 | { 164 | return CGRectMake(0, self.parentViewController.topLayoutGuide.length, self.parentViewController.view.frame.size.width, self.parentViewController.view.frame.size.height - self.parentViewController.topLayoutGuide.length); 165 | } 166 | 167 | #pragma mark - 168 | #pragma mark Public Methods 169 | 170 | + (instancetype)openMenuInViewController:(UIViewController *)viewController items:(NSArray *)menuItems dismissBlock:dismissBlock reorderBlock:reorderBlock deleteBlock:deleteBlock 171 | { 172 | [self blockUserInteraction:YES]; 173 | KEYPullDownMenu *pullDownView = [[KEYPullDownMenu alloc] init]; 174 | pullDownView.dismissBlock = dismissBlock; 175 | pullDownView.reorderBlock = reorderBlock; 176 | pullDownView.deleteBlock = deleteBlock; 177 | [pullDownView setParentViewController:viewController]; 178 | [pullDownView setFrame:pullDownView.initialFrame]; 179 | [pullDownView setItems:menuItems.mutableCopy]; 180 | [pullDownView setTag:kKEYPullDownViewTag]; 181 | [viewController.view addSubview:pullDownView]; 182 | [UIView animateWithDuration:kKEYPullDownAnimationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations: 183 | ^{ 184 | [pullDownView setFrame:pullDownView.finalFrame]; 185 | } 186 | completion:^(BOOL finished) 187 | { 188 | NSString *keyPath = @"position.y"; 189 | CABasicAnimation *positionAnimation = [CABasicAnimation animationWithKeyPath:keyPath];; 190 | positionAnimation.fromValue = @(CGRectGetMidY(pullDownView.frame)); 191 | positionAnimation.toValue = @(CGRectGetMidY(pullDownView.frame)-kKEYPullDownAnimationBounceHeight); 192 | positionAnimation.duration = 0.1; 193 | positionAnimation.beginTime = 0.0; 194 | 195 | id finalValue = @(CGRectGetMidY(pullDownView.finalFrame)); 196 | [pullDownView.layer setValue:finalValue forKeyPath:keyPath]; 197 | SKBounceAnimation *bounceAnimation = [SKBounceAnimation animationWithKeyPath:keyPath]; 198 | bounceAnimation.fromValue = [NSNumber numberWithFloat:CGRectGetMidY(pullDownView.finalFrame) -kKEYPullDownAnimationBounceHeight]; 199 | bounceAnimation.toValue = finalValue; 200 | bounceAnimation.numberOfBounces = 2; 201 | bounceAnimation.shouldOvershoot = NO; 202 | bounceAnimation.beginTime = 0.1; 203 | bounceAnimation.duration = 0.5; 204 | 205 | CAAnimationGroup *group = [CAAnimationGroup animation]; 206 | [group setDuration:.57]; 207 | [group setAnimations:[NSArray arrayWithObjects:positionAnimation, bounceAnimation, nil]]; 208 | 209 | [pullDownView.layer addAnimation:group forKey:@"bounceAnimation"]; 210 | [self blockUserInteraction:NO]; 211 | }]; 212 | 213 | return pullDownView; 214 | } 215 | 216 | + (instancetype)dismissInViewController:(UIViewController *)viewController 217 | { 218 | [self blockUserInteraction:YES]; 219 | KEYPullDownMenu *pullDownView = (KEYPullDownMenu *)[viewController.view viewWithTag:kKEYPullDownViewTag]; 220 | [pullDownView.layer removeAllAnimations]; 221 | [UIView animateWithDuration:kKEYPullDownAnimationDuration animations:^{ 222 | [pullDownView setFrame:pullDownView.initialFrame]; 223 | } completion:^(BOOL finished) { 224 | [pullDownView removeFromSuperview]; 225 | [self blockUserInteraction:NO]; 226 | }]; 227 | 228 | return pullDownView; 229 | } 230 | 231 | + (void)blockUserInteraction:(BOOL)block 232 | { 233 | [[[UIApplication sharedApplication] keyWindow] setUserInteractionEnabled:!block]; 234 | } 235 | 236 | @end 237 | 238 | @implementation KEYPullDownMenuItem 239 | { 240 | NSString *_name; 241 | BOOL _deletable; 242 | } 243 | 244 | - (id)initWithName:(NSString *)name deletable:(BOOL)deletable 245 | { 246 | self = [super init]; 247 | if (!self) return nil; 248 | 249 | _name = name; 250 | _deletable = deletable; 251 | self.dictionary = [NSMutableDictionary new]; 252 | 253 | return self; 254 | } 255 | 256 | + (instancetype)menuItemNamed:(NSString *)name deletable:(BOOL)deletable 257 | { 258 | return [[KEYPullDownMenuItem alloc] initWithName:name deletable:deletable]; 259 | } 260 | 261 | - (NSString *)name 262 | { 263 | return _name; 264 | } 265 | 266 | - (BOOL)deletable 267 | { 268 | return _deletable; 269 | } 270 | 271 | @end 272 | 273 | @implementation KEYPullDownMenuCell 274 | 275 | - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier 276 | { 277 | self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]; 278 | if (!self) return nil; 279 | 280 | self.textLabel.font = [UIFont boldSystemFontOfSize:23]; 281 | self.textLabel.textColor = [UIColor whiteColor]; 282 | self.textLabel.highlightedTextColor = [UIColor blackColor]; 283 | self.tintColor = [UIColor whiteColor]; 284 | 285 | return self; 286 | } 287 | 288 | @end 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Maximilian Mackh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Introduction 2 | 3 | A pull down menu, similar to notification center on iOS that supports an unlimited number of items. Items can either be selected, deleted or reordered. The control is aimed at providing context for switching data within the same view controller. 4 | 5 | by [@mmackh](https://twitter.com/mmackh) 6 | 7 | #Live Demo 8 | 9 | ![](https://raw.githubusercontent.com/mmackh/KEYPullDownMenu/master/demo.gif) 10 | 11 | #Example Usage 12 | 13 | ``` 14 | - (IBAction)togglePullDownMenu:(id)sender 15 | { 16 | [self.pullDownMenuButton setSelected:!self.pullDownMenuButton.selected]; 17 | BOOL menuVisible = self.pullDownMenuButton.selected; 18 | 19 | if (menuVisible) 20 | { 21 | self.searchBar.text = @""; 22 | [self.searchBar resignFirstResponder]; 23 | 24 | __weak typeof(self) weakSelf = self; 25 | KEYPullDownMenuItem *activeMenu = [KEYPullDownMenuItem menuItemNamed:@"HP Restaurant" deletable:NO]; 26 | [activeMenu setActive:YES]; 27 | 28 | NSMutableArray *pullDownItems = [NSMutableArray new]; 29 | for (PCSmartOrderTableStation *tableStation in self.controller.currentVenue.tableStations) 30 | { 31 | KEYPullDownMenuItem *item = [KEYPullDownMenuItem menuItemNamed:tableStation.name deletable:NO]; 32 | [item setActive:(_currentTableStation == tableStation)]; 33 | [pullDownItems addObject:item]; 34 | } 35 | 36 | KEYPullDownMenu *pullDownMenu = [KEYPullDownMenu openMenuInViewController:self items:pullDownItems 37 | dismissBlock:^(KEYPullDownMenuItem *selectedItem, NSInteger selectedRow) 38 | { 39 | PCSmartOrderTableStation *temporaryTableStation = weakSelf.controller.currentVenue.tableStations[selectedRow]; 40 | if (temporaryTableStation != _currentTableStation) 41 | { 42 | _currentTableStation = temporaryTableStation; 43 | weakSelf.controller.currentVenue.currentTableStation = _currentTableStation; 44 | 45 | [weakSelf.collectionView setContentOffset:CGPointZero]; 46 | [weakSelf updateView]; 47 | }; 48 | [weakSelf togglePullDownMenu:nil]; 49 | } 50 | reorderBlock:nil deleteBlock:nil]; 51 | pullDownMenu.backgroundColor = [UIColor colorWithWhite:0.3 alpha:0.95]; 52 | } 53 | else 54 | { 55 | [KEYPullDownMenu dismissInViewController:self]; 56 | } 57 | } 58 | ``` 59 | 60 | #Attribution 61 | 62 | ##SKBounceAnimation 63 | 64 | Copyright (c) 2012 Soroush Khanlou (http://khanlou.com/) 65 | 66 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 67 | 68 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 69 | 70 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 71 | 72 | ##BVReorderTableView 73 | Created by Benjamin Vogelzang on 3/5/13. 74 | Copyright (c) 2013 Ben Vogelzang. 75 | 76 | Permission is hereby granted, free of charge, to any person obtaining a copy 77 | of this software and associated documentation files (the "Software"), to deal 78 | in the Software without restriction, including without limitation the rights 79 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 80 | copies of the Software, and to permit persons to whom the Software is 81 | furnished to do so, subject to the following conditions: 82 | 83 | The above copyright notice and this permission notice shall be included in 84 | all copies or substantial portions of the Software. 85 | 86 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 87 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 88 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 89 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 90 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 91 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 92 | THE SOFTWARE. 93 | -------------------------------------------------------------------------------- /SKBounceAnimation.h: -------------------------------------------------------------------------------- 1 | // 2 | // SKBounceAnimation.h 3 | // SKBounceAnimation 4 | // 5 | // Created by Soroush Khanlou on 6/15/12. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | typedef enum { 13 | SKBounceAnimationStiffnessLight, 14 | SKBounceAnimationStiffnessMedium, 15 | SKBounceAnimationStiffnessHeavy 16 | } SKBounceAnimationStiffness; 17 | 18 | @interface SKBounceAnimation : CAKeyframeAnimation 19 | 20 | @property (nonatomic, retain) id fromValue; 21 | @property (nonatomic, retain) id byValue; 22 | @property (nonatomic, retain) id toValue; 23 | @property (nonatomic, assign) NSUInteger numberOfBounces; 24 | @property (nonatomic, assign) BOOL shouldOvershoot; //default YES 25 | @property (nonatomic, assign) BOOL shake; //if shaking, set fromValue to the furthest value, and toValue to the current value 26 | @property (nonatomic, assign) SKBounceAnimationStiffness stiffness; 27 | 28 | + (SKBounceAnimation*) animationWithKeyPath:(NSString*)keyPath; 29 | 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /SKBounceAnimation.m: -------------------------------------------------------------------------------- 1 | // 2 | // SKBounceAnimation.m 3 | // SKBounceAnimation 4 | // 5 | // Created by Soroush Khanlou on 6/15/12. 6 | // Copyright (c) 2012 __MyCompanyName__. All rights reserved. 7 | // 8 | 9 | #import "SKBounceAnimation.h" 10 | 11 | /* 12 | Keypaths: 13 | 14 | Float animations: 15 | anchorPoint 16 | cornerRadius 17 | borderWidth 18 | opacity 19 | shadowRadius 20 | shadowOpacity 21 | zPosition 22 | 23 | Point/size animations: 24 | position 25 | shadowOffset 26 | 27 | Rect animations: 28 | bounds 29 | frame - not strictly animatable, use bounds 30 | contentsRect 31 | 32 | Colors: 33 | backgroundColor 34 | borderColor 35 | shadowColor 36 | 37 | CATransform3D: 38 | transform 39 | 40 | 41 | Meaningless: 42 | backgroundFilters 43 | compositingFilter 44 | contents 45 | doubleSided 46 | filters 47 | hidden 48 | mask 49 | masksToBounds 50 | sublayers 51 | sublayerTransform 52 | 53 | */ 54 | 55 | @interface SKBounceAnimation (Private) 56 | 57 | - (void) createValueArray; 58 | - (NSArray*) valueArrayForStartValue:(CGFloat)startValue endValue:(CGFloat)endValue; 59 | - (CGPathRef) createPathFromXValues:(NSArray*)xValues yValues:(NSArray*)yValues; 60 | - (NSArray*) createRectArrayFromXValues:(NSArray*)xValues yValues:(NSArray*)yValues widths:(NSArray*)widths heights:(NSArray*)heights; 61 | - (NSArray*) createColorArrayFromRed:(NSArray*)redValues green:(NSArray*)greenValues blue:(NSArray*)blueValues alpha:(NSArray*)alphaValues; 62 | - (NSArray*) createTransformArrayFromM11:(NSArray*)m11 M12:(NSArray*)m12 M13:(NSArray*)m13 M14:(NSArray*)m14 63 | M21:(NSArray*)m21 M22:(NSArray*)m22 M23:(NSArray*)m23 M14:(NSArray*)m24 64 | M31:(NSArray*)m31 M32:(NSArray*)m32 M33:(NSArray*)m33 M14:(NSArray*)m34 65 | M41:(NSArray*)m41 M42:(NSArray*)m42 M43:(NSArray*)m43 M14:(NSArray*)m44; 66 | 67 | @end 68 | 69 | @implementation SKBounceAnimation 70 | 71 | @synthesize fromValue, byValue, toValue, numberOfBounces, shouldOvershoot; 72 | 73 | + (SKBounceAnimation*) animationWithKeyPath:(NSString*)keyPath { 74 | return [[self alloc] initWithKeyPath:keyPath]; 75 | } 76 | 77 | - (id) initWithKeyPath:(NSString*)keyPath { 78 | self = [super init]; 79 | if (self) { 80 | super.keyPath = keyPath; 81 | self.numberOfBounces = 2; 82 | self.shouldOvershoot = YES; 83 | self.stiffness = SKBounceAnimationStiffnessMedium; 84 | } 85 | return self; 86 | } 87 | 88 | - (void) setFromValue:(id)newFromValue { 89 | [super setValue:newFromValue forKey:@"fromValueKey"]; 90 | [self createValueArray]; 91 | } 92 | 93 | - (id) fromValue { 94 | return [super valueForKey:@"fromValueKey"]; 95 | } 96 | 97 | - (void) setToValue:(id)newToValue { 98 | [super setValue:newToValue forKey:@"toValueKey"]; 99 | [self createValueArray]; 100 | } 101 | 102 | - (id) toValue { 103 | return [super valueForKey:@"toValueKey"]; 104 | } 105 | 106 | - (void) setDuration:(CFTimeInterval)newDuration { 107 | [super setDuration:newDuration]; 108 | [self createValueArray]; 109 | } 110 | 111 | - (void) setNumberOfBounces:(NSUInteger)newNumberOfBounces { 112 | [super setValue:[NSNumber numberWithUnsignedInt:(int)newNumberOfBounces] forKey:@"numberOfBouncesKey"]; 113 | [self createValueArray]; 114 | } 115 | 116 | - (NSUInteger) numberOfBounces { 117 | return [[super valueForKey:@"numberOfBouncesKey"] unsignedIntValue]; 118 | } 119 | 120 | - (void) setStiffness:(SKBounceAnimationStiffness)stiffness { 121 | [super setValue:@(stiffness) forKey:@"stifnessKey"]; 122 | [self createValueArray]; 123 | } 124 | 125 | - (SKBounceAnimationStiffness) stiffness { 126 | return (int)[[super valueForKey:@"stifnessKey"] integerValue]; 127 | } 128 | 129 | - (void) setShouldOvershoot:(BOOL)newShouldOvershoot { 130 | [super setValue:[NSNumber numberWithBool:newShouldOvershoot] forKey:@"shouldOvershootKey"]; 131 | [self createValueArray]; 132 | } 133 | 134 | - (BOOL) shouldOvershoot { 135 | return [[super valueForKey:@"shouldOvershootKey"] boolValue]; 136 | } 137 | 138 | - (void) setShake:(BOOL)newShake { 139 | [super setValue:[NSNumber numberWithBool:newShake] forKey:@"shakeKey"]; 140 | [self createValueArray]; 141 | } 142 | 143 | - (BOOL) shake { 144 | return [[super valueForKey:@"shakeKey"] boolValue]; 145 | } 146 | 147 | - (void) createValueArray { 148 | if (self.fromValue && self.toValue && self.duration) { 149 | if ([self.fromValue isKindOfClass:[NSNumber class]] && [self.toValue isKindOfClass:[NSNumber class]]) { 150 | self.values = [self valueArrayForStartValue:[self.fromValue floatValue] endValue:[self.toValue floatValue]]; 151 | } else if ([self.fromValue isKindOfClass:[UIColor class]] && [self.toValue isKindOfClass:[UIColor class]]) { 152 | const CGFloat *fromComponents = CGColorGetComponents(((UIColor*)self.fromValue).CGColor); 153 | const CGFloat *toComponents = CGColorGetComponents(((UIColor*)self.toValue).CGColor); 154 | // NSLog(@"from %0.2f %0.2f %0.2f %0.2f", fromComponents[0], fromComponents[1], fromComponents[2], fromComponents[3]); 155 | // NSLog(@"to %0.2f %0.2f %0.2f %0.2f", toComponents[0], toComponents[1], toComponents[2], toComponents[3]); 156 | self.values = [self createColorArrayFromRed: 157 | [self valueArrayForStartValue:fromComponents[0] endValue:toComponents[0]] 158 | green: 159 | [self valueArrayForStartValue:fromComponents[1] endValue:toComponents[1]] 160 | blue: 161 | [self valueArrayForStartValue:fromComponents[2] endValue:toComponents[2]] 162 | alpha: 163 | [self valueArrayForStartValue:fromComponents[3] endValue:toComponents[3]]]; 164 | } else if ([self.fromValue isKindOfClass:[NSValue class]] && [self.toValue isKindOfClass:[NSValue class]]) { 165 | NSString *valueType = [NSString stringWithCString:[self.fromValue objCType] encoding:NSStringEncodingConversionAllowLossy]; 166 | if ([valueType rangeOfString:@"CGRect"].location == 1) { 167 | CGRect fromRect = [self.fromValue CGRectValue]; 168 | CGRect toRect = [self.toValue CGRectValue]; 169 | self.values = [self createRectArrayFromXValues: 170 | [self valueArrayForStartValue:fromRect.origin.x endValue:toRect.origin.x] 171 | yValues: 172 | [self valueArrayForStartValue:fromRect.origin.y endValue:toRect.origin.y] 173 | widths: 174 | [self valueArrayForStartValue:fromRect.size.width endValue:toRect.size.width] 175 | heights: 176 | [self valueArrayForStartValue:fromRect.size.height endValue:toRect.size.height]]; 177 | 178 | } else if ([valueType rangeOfString:@"CGPoint"].location == 1) { 179 | CGPoint fromPoint = [self.fromValue CGPointValue]; 180 | CGPoint toPoint = [self.toValue CGPointValue]; 181 | CGPathRef path = createPathFromXYValues([self valueArrayForStartValue:fromPoint.x endValue:toPoint.x], [self valueArrayForStartValue:fromPoint.y endValue:toPoint.y]); 182 | self.path = path; 183 | CGPathRelease(path); 184 | 185 | } else if ([valueType rangeOfString:@"CATransform3D"].location == 1) { 186 | CATransform3D fromTransform = [self.fromValue CATransform3DValue]; 187 | CATransform3D toTransform = [self.toValue CATransform3DValue]; 188 | 189 | // NSLog(@"from [%0.2f %0.2f %0.2f %0.2f; %0.2f %0.2f %0.2f %0.2f; %0.2f %0.2f %0.2f %0.2f; %0.2f %0.2f %0.2f %0.2f]", fromTransform.m11, fromTransform.m12, fromTransform.m13, fromTransform.m14, fromTransform.m21, fromTransform.m22, fromTransform.m23, fromTransform.m24, fromTransform.m31, fromTransform.m32, fromTransform.m33, fromTransform.m34, fromTransform.m41, fromTransform.m42, fromTransform.m43, fromTransform.m44); 190 | // 191 | // NSLog(@"to [%0.2f %0.2f %0.2f %0.2f; %0.2f %0.2f %0.2f %0.2f; %0.2f %0.2f %0.2f %0.2f; %0.2f %0.2f %0.2f %0.2f]", toTransform.m11, toTransform.m12, toTransform.m13, toTransform.m14, toTransform.m21, toTransform.m22, toTransform.m23, toTransform.m24, toTransform.m31, toTransform.m32, toTransform.m33, toTransform.m34, toTransform.m41, toTransform.m42, toTransform.m43, toTransform.m44); 192 | 193 | self.values = [self createTransformArrayFromM11: 194 | [self valueArrayForStartValue:fromTransform.m11 endValue:toTransform.m11] 195 | M12: 196 | [self valueArrayForStartValue:fromTransform.m12 endValue:toTransform.m12] 197 | M13: 198 | [self valueArrayForStartValue:fromTransform.m13 endValue:toTransform.m13] 199 | M14: 200 | [self valueArrayForStartValue:fromTransform.m14 endValue:toTransform.m14] 201 | M21: 202 | [self valueArrayForStartValue:fromTransform.m21 endValue:toTransform.m21] 203 | M22: 204 | [self valueArrayForStartValue:fromTransform.m22 endValue:toTransform.m22] 205 | M23: 206 | [self valueArrayForStartValue:fromTransform.m23 endValue:toTransform.m23] 207 | M24: 208 | [self valueArrayForStartValue:fromTransform.m24 endValue:toTransform.m24] 209 | M31: 210 | [self valueArrayForStartValue:fromTransform.m31 endValue:toTransform.m31] 211 | M32: 212 | [self valueArrayForStartValue:fromTransform.m32 endValue:toTransform.m32] 213 | M33: 214 | [self valueArrayForStartValue:fromTransform.m33 endValue:toTransform.m33] 215 | M34: 216 | [self valueArrayForStartValue:fromTransform.m34 endValue:toTransform.m34] 217 | M41: 218 | [self valueArrayForStartValue:fromTransform.m41 endValue:toTransform.m41] 219 | M42: 220 | [self valueArrayForStartValue:fromTransform.m42 endValue:toTransform.m42] 221 | M43: 222 | [self valueArrayForStartValue:fromTransform.m43 endValue:toTransform.m43] 223 | M44: 224 | [self valueArrayForStartValue:fromTransform.m44 endValue:toTransform.m44] 225 | ]; 226 | } else if ([valueType rangeOfString:@"CGSize"].location == 1) { 227 | CGSize fromSize = [self.fromValue CGSizeValue]; 228 | CGSize toSize = [self.toValue CGSizeValue]; 229 | CGPathRef path = createPathFromXYValues( 230 | [self valueArrayForStartValue:fromSize.width endValue:toSize.width], 231 | [self valueArrayForStartValue:fromSize.height endValue:toSize.height] 232 | ); 233 | self.path = path; 234 | CGPathRelease(path); 235 | } 236 | 237 | } 238 | self.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; 239 | } 240 | } 241 | 242 | - (NSArray*) createRectArrayFromXValues:(NSArray*)xValues yValues:(NSArray*)yValues widths:(NSArray*)widths heights:(NSArray*)heights { 243 | NSAssert(xValues.count == yValues.count && xValues.count == widths.count && xValues.count == heights.count, @"array must have arrays of equal size"); 244 | 245 | NSUInteger numberOfRects = xValues.count; 246 | NSMutableArray *values = [NSMutableArray arrayWithCapacity:numberOfRects]; 247 | CGRect value; 248 | 249 | for (NSInteger i = 1; i < numberOfRects; i++) { 250 | value = CGRectMake( 251 | [[xValues objectAtIndex:i] floatValue], 252 | [[yValues objectAtIndex:i] floatValue], 253 | [[widths objectAtIndex:i] floatValue], 254 | [[heights objectAtIndex:i] floatValue] 255 | ); 256 | [values addObject:[NSValue valueWithCGRect:value]]; 257 | } 258 | return values; 259 | } 260 | 261 | static CGPathRef createPathFromXYValues(NSArray *xValues, NSArray *yValues) { 262 | NSUInteger numberOfPoints = xValues.count; 263 | CGMutablePathRef path = CGPathCreateMutable(); 264 | CGPoint value; 265 | value = CGPointMake([[xValues objectAtIndex:0] floatValue], [[yValues objectAtIndex:0] floatValue]); 266 | CGPathMoveToPoint(path, NULL, value.x, value.y); 267 | 268 | for (NSInteger i = 1; i < numberOfPoints; i++) { 269 | value = CGPointMake([[xValues objectAtIndex:i] floatValue], [[yValues objectAtIndex:i] floatValue]); 270 | CGPathAddLineToPoint(path, NULL, value.x, value.y); 271 | } 272 | return path; 273 | } 274 | 275 | - (NSArray*) createTransformArrayFromM11:(NSArray*)m11 M12:(NSArray*)m12 M13:(NSArray*)m13 M14:(NSArray*)m14 276 | M21:(NSArray*)m21 M22:(NSArray*)m22 M23:(NSArray*)m23 M24:(NSArray*)m24 277 | M31:(NSArray*)m31 M32:(NSArray*)m32 M33:(NSArray*)m33 M34:(NSArray*)m34 278 | M41:(NSArray*)m41 M42:(NSArray*)m42 M43:(NSArray*)m43 M44:(NSArray*)m44 { 279 | NSUInteger numberOfTransforms = m11.count; 280 | NSMutableArray *values = [NSMutableArray arrayWithCapacity:numberOfTransforms]; 281 | CATransform3D value; 282 | 283 | for (NSInteger i = 1; i < numberOfTransforms; i++) { 284 | value = CATransform3DIdentity; 285 | value.m11 = [[m11 objectAtIndex:i] floatValue]; 286 | value.m12 = [[m12 objectAtIndex:i] floatValue]; 287 | value.m13 = [[m13 objectAtIndex:i] floatValue]; 288 | value.m14 = [[m14 objectAtIndex:i] floatValue]; 289 | 290 | value.m21 = [[m21 objectAtIndex:i] floatValue]; 291 | value.m22 = [[m22 objectAtIndex:i] floatValue]; 292 | value.m23 = [[m23 objectAtIndex:i] floatValue]; 293 | value.m24 = [[m24 objectAtIndex:i] floatValue]; 294 | 295 | value.m31 = [[m31 objectAtIndex:i] floatValue]; 296 | value.m32 = [[m32 objectAtIndex:i] floatValue]; 297 | value.m33 = [[m33 objectAtIndex:i] floatValue]; 298 | value.m44 = [[m34 objectAtIndex:i] floatValue]; 299 | 300 | value.m41 = [[m41 objectAtIndex:i] floatValue]; 301 | value.m42 = [[m42 objectAtIndex:i] floatValue]; 302 | value.m43 = [[m43 objectAtIndex:i] floatValue]; 303 | value.m44 = [[m44 objectAtIndex:i] floatValue]; 304 | 305 | [values addObject:[NSValue valueWithCATransform3D:value]]; 306 | } 307 | return values; 308 | } 309 | 310 | - (NSArray*) createColorArrayFromRed:(NSArray*)redValues green:(NSArray*)greenValues blue:(NSArray*)blueValues alpha:(NSArray*)alphaValues { 311 | NSAssert(redValues.count == blueValues.count && redValues.count == greenValues.count && redValues.count == alphaValues.count, @"array must have arrays of equal size"); 312 | 313 | NSUInteger numberOfColors = redValues.count; 314 | NSMutableArray *values = [NSMutableArray arrayWithCapacity:numberOfColors]; 315 | UIColor *value; 316 | 317 | for (NSInteger i = 1; i < numberOfColors; i++) { 318 | value = [UIColor colorWithRed:[[redValues objectAtIndex:i] floatValue] 319 | green:[[greenValues objectAtIndex:i] floatValue] 320 | blue:[[blueValues objectAtIndex:i] floatValue] 321 | alpha:[[alphaValues objectAtIndex:i] floatValue]]; 322 | [values addObject:(id)value.CGColor]; 323 | } 324 | return values; 325 | } 326 | 327 | - (NSArray*) valueArrayForStartValue:(CGFloat)startValue endValue:(CGFloat)endValue { 328 | NSInteger steps = 60*self.duration; //60 fps desired 329 | 330 | CGFloat stiffnessCoefficient = 0.1f; 331 | if (self.stiffness == SKBounceAnimationStiffnessHeavy) { 332 | stiffnessCoefficient = 0.001f; 333 | } else if (self.stiffness == SKBounceAnimationStiffnessLight) { 334 | stiffnessCoefficient = 5.0f; 335 | } 336 | 337 | CGFloat alpha = 0; 338 | if (startValue == endValue) { 339 | alpha = log2f(stiffnessCoefficient)/steps; 340 | } else { 341 | alpha = log2f(stiffnessCoefficient/fabsf(endValue - startValue))/steps; 342 | } 343 | if (alpha > 0) { 344 | alpha = -1.0f*alpha; 345 | } 346 | CGFloat numberOfPeriods = self.numberOfBounces/2 + 0.5; 347 | CGFloat omega = numberOfPeriods * 2*M_PI/steps; 348 | 349 | //uncomment this to get the equation of motion 350 | // NSLog(@"y = %0.2f * e^(%0.5f*x)*cos(%0.10f*x) + %0.0f over %d frames", startValue - endValue, alpha, omega, endValue, steps); 351 | 352 | NSMutableArray *values = [NSMutableArray arrayWithCapacity:steps]; 353 | CGFloat value = 0; 354 | 355 | CGFloat oscillationComponent; 356 | CGFloat coefficient; 357 | 358 | // conforms to y = A * e^(-alpha*t)*cos(omega*t) 359 | for (NSInteger t = 0; t < steps; t++) { 360 | //decaying mass-spring-damper solution with initial displacement 361 | 362 | if (self.shake) { 363 | oscillationComponent = sin(omega*t); 364 | } else { 365 | oscillationComponent = cos(omega*t); 366 | } 367 | 368 | coefficient = (startValue - endValue); 369 | 370 | if (!self.shouldOvershoot) { 371 | oscillationComponent = fabsf(oscillationComponent); 372 | } 373 | 374 | value = coefficient * pow(2.71, alpha*t) * oscillationComponent + endValue; 375 | 376 | 377 | 378 | [values addObject:[NSNumber numberWithFloat:value]]; 379 | } 380 | return values; 381 | } 382 | 383 | 384 | @end 385 | 386 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mmackh/KEYPullDownMenu/c56434b6364ec7a822ea5137c38245e0676712af/demo.gif --------------------------------------------------------------------------------