MGSwipeTableCell.m 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389
  1. /*
  2. * MGSwipeTableCell is licensed under MIT license. See LICENSE.md file for more information.
  3. * Copyright (c) 2016 Imanol Fernandez @MortimerGoro
  4. */
  5. #import "MGSwipeTableCell.h"
  6. #pragma mark Input Overlay Helper Class
  7. /** Used to capture table input while swipe buttons are visible*/
  8. @interface MGSwipeTableInputOverlay : UIView
  9. @property (nonatomic, weak) MGSwipeTableCell * currentCell;
  10. @end
  11. @implementation MGSwipeTableInputOverlay
  12. -(id) initWithFrame:(CGRect)frame
  13. {
  14. if (self = [super initWithFrame:frame]) {
  15. self.backgroundColor = [UIColor clearColor];
  16. self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  17. }
  18. return self;
  19. }
  20. -(UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event
  21. {
  22. if (event == nil) {
  23. return nil;
  24. }
  25. if (!_currentCell) {
  26. [self removeFromSuperview];
  27. return nil;
  28. }
  29. CGPoint p = [self convertPoint:point toView:_currentCell];
  30. if (_currentCell && (_currentCell.hidden || CGRectContainsPoint(_currentCell.bounds, p))) {
  31. return nil;
  32. }
  33. BOOL hide = YES;
  34. if (_currentCell && _currentCell.delegate && [_currentCell.delegate respondsToSelector:@selector(swipeTableCell:shouldHideSwipeOnTap:)]) {
  35. hide = [_currentCell.delegate swipeTableCell:_currentCell shouldHideSwipeOnTap:p];
  36. }
  37. if (hide) {
  38. [_currentCell hideSwipeAnimated:YES];
  39. }
  40. return _currentCell.touchOnDismissSwipe ? nil : self;
  41. }
  42. @end
  43. #pragma mark Button Container View and transitions
  44. @interface MGSwipeButtonsView : UIView
  45. @property (nonatomic, weak) MGSwipeTableCell * cell;
  46. @property (nonatomic, strong) UIColor * backgroundColorCopy;
  47. @end
  48. @implementation MGSwipeButtonsView
  49. {
  50. NSArray * _buttons;
  51. UIView * _container;
  52. BOOL _fromLeft;
  53. UIView * _expandedButton;
  54. UIView * _expandedButtonAnimated;
  55. UIView * _expansionBackground;
  56. UIView * _expansionBackgroundAnimated;
  57. CGRect _expandedButtonBoundsCopy;
  58. MGSwipeExpansionLayout _expansionLayout;
  59. CGFloat _expansionOffset;
  60. CGFloat _buttonsDistance;
  61. BOOL _autoHideExpansion;
  62. }
  63. #pragma mark Layout
  64. -(instancetype) initWithButtons:(NSArray*) buttonsArray direction:(MGSwipeDirection) direction differentWidth:(BOOL) differentWidth buttonsDistance:(CGFloat) buttonsDistance
  65. {
  66. CGFloat containerWidth = 0;
  67. CGSize maxSize = CGSizeZero;
  68. UIView* lastButton = [buttonsArray lastObject];
  69. for (UIView * button in buttonsArray) {
  70. containerWidth += button.bounds.size.width + (lastButton == button ? 0 : buttonsDistance);
  71. maxSize.width = MAX(maxSize.width, button.bounds.size.width);
  72. maxSize.height = MAX(maxSize.height, button.bounds.size.height);
  73. }
  74. if (!differentWidth) {
  75. containerWidth = maxSize.width * buttonsArray.count + buttonsDistance * (buttonsArray.count - 1);
  76. }
  77. if (self = [super initWithFrame:CGRectMake(0, 0, containerWidth, maxSize.height)]) {
  78. _fromLeft = direction == MGSwipeDirectionLeftToRight;
  79. _buttonsDistance = buttonsDistance;
  80. _container = [[UIView alloc] initWithFrame:self.bounds];
  81. _container.clipsToBounds = YES;
  82. _container.backgroundColor = [UIColor clearColor];
  83. [self addSubview:_container];
  84. _buttons = _fromLeft ? buttonsArray: [[buttonsArray reverseObjectEnumerator] allObjects];
  85. for (UIView * button in _buttons) {
  86. if ([button isKindOfClass:[UIButton class]]) {
  87. UIButton * btn = (UIButton*)button;
  88. [btn removeTarget:nil action:@selector(mgButtonClicked:) forControlEvents:UIControlEventTouchUpInside]; //Remove all targets to avoid problems with reused buttons among many cells
  89. [btn addTarget:self action:@selector(mgButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
  90. }
  91. if (!differentWidth) {
  92. button.frame = CGRectMake(0, 0, maxSize.width, maxSize.height);
  93. }
  94. button.autoresizingMask = UIViewAutoresizingFlexibleHeight;
  95. [_container insertSubview:button atIndex: _fromLeft ? 0: _container.subviews.count];
  96. }
  97. [self resetButtons];
  98. }
  99. return self;
  100. }
  101. -(void) dealloc
  102. {
  103. for (UIView * button in _buttons) {
  104. if ([button isKindOfClass:[UIButton class]]) {
  105. [(UIButton *)button removeTarget:self action:@selector(mgButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
  106. }
  107. }
  108. }
  109. -(void) resetButtons
  110. {
  111. CGFloat offsetX = 0;
  112. UIView* lastButton = [_buttons lastObject];
  113. for (UIView * button in _buttons) {
  114. button.frame = CGRectMake(offsetX, 0, button.bounds.size.width, self.bounds.size.height);
  115. button.autoresizingMask = UIViewAutoresizingFlexibleHeight;
  116. offsetX += button.bounds.size.width + (lastButton == button ? 0 : _buttonsDistance);
  117. }
  118. }
  119. -(void) layoutExpansion: (CGFloat) offset
  120. {
  121. _expansionOffset = offset;
  122. _container.frame = CGRectMake(_fromLeft ? 0: self.bounds.size.width - offset, 0, offset, self.bounds.size.height);
  123. if (_expansionBackgroundAnimated && _expandedButtonAnimated) {
  124. _expansionBackgroundAnimated.frame = [self expansionBackgroundRect:_expandedButtonAnimated];
  125. }
  126. }
  127. -(void) layoutSubviews
  128. {
  129. [super layoutSubviews];
  130. if (_expandedButton) {
  131. [self layoutExpansion:_expansionOffset];
  132. }
  133. else {
  134. _container.frame = self.bounds;
  135. }
  136. }
  137. -(CGRect) expansionBackgroundRect: (UIView *) button
  138. {
  139. CGFloat extra = 100.0f; //extra size to avoid expansion background size issue on iOS 7.0
  140. if (_fromLeft) {
  141. return CGRectMake(-extra, 0, button.frame.origin.x + extra, _container.bounds.size.height);
  142. }
  143. else {
  144. return CGRectMake(button.frame.origin.x + button.bounds.size.width, 0,
  145. _container.bounds.size.width - (button.frame.origin.x + button.bounds.size.width ) + extra
  146. ,_container.bounds.size.height);
  147. }
  148. }
  149. -(void) expandToOffset:(CGFloat) offset settings:(MGSwipeExpansionSettings*) settings
  150. {
  151. if (settings.buttonIndex < 0 || settings.buttonIndex >= _buttons.count) {
  152. return;
  153. }
  154. if (!_expandedButton) {
  155. _expandedButton = [_buttons objectAtIndex: _fromLeft ? settings.buttonIndex : _buttons.count - settings.buttonIndex - 1];
  156. CGRect previusRect = _container.frame;
  157. [self layoutExpansion:offset];
  158. [self resetButtons];
  159. if (!_fromLeft) { //Fix expansion animation for right buttons
  160. for (UIView * button in _buttons) {
  161. CGRect frame = button.frame;
  162. frame.origin.x += _container.bounds.size.width - previusRect.size.width;
  163. button.frame = frame;
  164. }
  165. }
  166. _expansionBackground = [[UIView alloc] initWithFrame:[self expansionBackgroundRect:_expandedButton]];
  167. _expansionBackground.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  168. if (settings.expansionColor) {
  169. _backgroundColorCopy = _expandedButton.backgroundColor;
  170. _expandedButton.backgroundColor = settings.expansionColor;
  171. }
  172. _expansionBackground.backgroundColor = _expandedButton.backgroundColor;
  173. if (UIColor.clearColor == _expandedButton.backgroundColor) {
  174. // Provides access to more complex content for display on the background
  175. _expansionBackground.layer.contents = _expandedButton.layer.contents;
  176. }
  177. [_container addSubview:_expansionBackground];
  178. _expansionLayout = settings.expansionLayout;
  179. CGFloat duration = _fromLeft ? _cell.leftExpansion.animationDuration : _cell.rightExpansion.animationDuration;
  180. [UIView animateWithDuration: duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
  181. _expandedButton.hidden = NO;
  182. if (_expansionLayout == MGSwipeExpansionLayoutCenter) {
  183. _expandedButtonBoundsCopy = _expandedButton.bounds;
  184. _expandedButton.layer.mask = nil;
  185. _expandedButton.layer.transform = CATransform3DIdentity;
  186. _expandedButton.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  187. [_expandedButton.superview bringSubviewToFront:_expandedButton];
  188. _expandedButton.frame = _container.bounds;
  189. _expansionBackground.frame = [self expansionBackgroundRect:_expandedButton];
  190. }
  191. else if (_expansionLayout == MGSwipeExpansionLayoutNone) {
  192. [_expandedButton.superview bringSubviewToFront:_expandedButton];
  193. _expansionBackground.frame = _container.bounds;
  194. }
  195. else if (_fromLeft) {
  196. _expandedButton.frame = CGRectMake(_container.bounds.size.width - _expandedButton.bounds.size.width, 0, _expandedButton.bounds.size.width, _expandedButton.bounds.size.height);
  197. _expandedButton.autoresizingMask|= UIViewAutoresizingFlexibleLeftMargin;
  198. _expansionBackground.frame = [self expansionBackgroundRect:_expandedButton];
  199. }
  200. else {
  201. _expandedButton.frame = CGRectMake(0, 0, _expandedButton.bounds.size.width, _expandedButton.bounds.size.height);
  202. _expandedButton.autoresizingMask|= UIViewAutoresizingFlexibleRightMargin;
  203. _expansionBackground.frame = [self expansionBackgroundRect:_expandedButton];
  204. }
  205. } completion:^(BOOL finished) {
  206. }];
  207. return;
  208. }
  209. [self layoutExpansion:offset];
  210. }
  211. -(void) endExpansionAnimated:(BOOL) animated
  212. {
  213. if (_expandedButton) {
  214. _expandedButtonAnimated = _expandedButton;
  215. if (_expansionBackgroundAnimated && _expansionBackgroundAnimated != _expansionBackground) {
  216. [_expansionBackgroundAnimated removeFromSuperview];
  217. }
  218. _expansionBackgroundAnimated = _expansionBackground;
  219. _expansionBackground = nil;
  220. _expandedButton = nil;
  221. if (_backgroundColorCopy) {
  222. _expansionBackgroundAnimated.backgroundColor = _backgroundColorCopy;
  223. _expandedButtonAnimated.backgroundColor = _backgroundColorCopy;
  224. _backgroundColorCopy = nil;
  225. }
  226. CGFloat duration = _fromLeft ? _cell.leftExpansion.animationDuration : _cell.rightExpansion.animationDuration;
  227. [UIView animateWithDuration: animated ? duration : 0.0 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
  228. _container.frame = self.bounds;
  229. if (_expansionLayout == MGSwipeExpansionLayoutCenter) {
  230. _expandedButtonAnimated.frame = _expandedButtonBoundsCopy;
  231. }
  232. [self resetButtons];
  233. _expansionBackgroundAnimated.frame = [self expansionBackgroundRect:_expandedButtonAnimated];
  234. } completion:^(BOOL finished) {
  235. [_expansionBackgroundAnimated removeFromSuperview];
  236. }];
  237. }
  238. else if (_expansionBackground) {
  239. [_expansionBackground removeFromSuperview];
  240. _expansionBackground = nil;
  241. }
  242. }
  243. -(UIView*) getExpandedButton
  244. {
  245. return _expandedButton;
  246. }
  247. #pragma mark Trigger Actions
  248. -(BOOL) handleClick: (id) sender fromExpansion:(BOOL) fromExpansion
  249. {
  250. bool autoHide = false;
  251. #pragma clang diagnostic push
  252. #pragma clang diagnostic ignored "-Wundeclared-selector"
  253. if ([sender respondsToSelector:@selector(callMGSwipeConvenienceCallback:)]) {
  254. //call convenience block callback if exits (usage of MGSwipeButton class is not compulsory)
  255. autoHide = [sender performSelector:@selector(callMGSwipeConvenienceCallback:) withObject:_cell];
  256. }
  257. #pragma clang diagnostic pop
  258. if (_cell.delegate && [_cell.delegate respondsToSelector:@selector(swipeTableCell:tappedButtonAtIndex:direction:fromExpansion:)]) {
  259. NSInteger index = [_buttons indexOfObject:sender];
  260. if (!_fromLeft) {
  261. index = _buttons.count - index - 1; //right buttons are reversed
  262. }
  263. autoHide|= [_cell.delegate swipeTableCell:_cell tappedButtonAtIndex:index direction:_fromLeft ? MGSwipeDirectionLeftToRight : MGSwipeDirectionRightToLeft fromExpansion:fromExpansion];
  264. }
  265. if (fromExpansion && autoHide) {
  266. _expandedButton = nil;
  267. _cell.swipeOffset = 0;
  268. }
  269. else if (autoHide) {
  270. [_cell hideSwipeAnimated:YES];
  271. }
  272. return autoHide;
  273. }
  274. //button listener
  275. -(void) mgButtonClicked: (id) sender
  276. {
  277. [self handleClick:sender fromExpansion:NO];
  278. }
  279. #pragma mark Transitions
  280. -(void) transitionStatic:(CGFloat) t
  281. {
  282. const CGFloat dx = self.bounds.size.width * (1.0 - t);
  283. CGFloat offsetX = 0;
  284. UIView* lastButton = [_buttons lastObject];
  285. for (UIView *button in _buttons) {
  286. CGRect frame = button.frame;
  287. frame.origin.x = offsetX + (_fromLeft ? dx : -dx);
  288. button.frame = frame;
  289. offsetX += frame.size.width + (button == lastButton ? 0 : _buttonsDistance);
  290. }
  291. }
  292. -(void) transitionDrag:(CGFloat) t
  293. {
  294. //No Op, nothing to do ;)
  295. }
  296. -(void) transitionClip:(CGFloat) t
  297. {
  298. CGFloat selfWidth = self.bounds.size.width;
  299. CGFloat offsetX = 0;
  300. UIView* lastButton = [_buttons lastObject];
  301. for (UIView *button in _buttons) {
  302. CGRect frame = button.frame;
  303. CGFloat dx = roundf(frame.size.width * 0.5 * (1.0 - t)) ;
  304. frame.origin.x = _fromLeft ? (selfWidth - frame.size.width - offsetX) * (1.0 - t) + offsetX + dx : offsetX * t - dx;
  305. button.frame = frame;
  306. if (_buttons.count > 1) {
  307. CAShapeLayer *maskLayer = [CAShapeLayer new];
  308. CGRect maskRect = CGRectMake(dx - 0.5, 0, frame.size.width - 2 * dx + 1.5, frame.size.height);
  309. CGPathRef path = CGPathCreateWithRect(maskRect, NULL);
  310. maskLayer.path = path;
  311. CGPathRelease(path);
  312. button.layer.mask = maskLayer;
  313. }
  314. offsetX += frame.size.width + (button == lastButton ? 0 : _buttonsDistance);
  315. }
  316. }
  317. -(void) transtitionFloatBorder:(CGFloat) t
  318. {
  319. CGFloat selfWidth = self.bounds.size.width;
  320. CGFloat offsetX = 0;
  321. UIView* lastButton = [_buttons lastObject];
  322. for (UIView *button in _buttons) {
  323. CGRect frame = button.frame;
  324. frame.origin.x = _fromLeft ? (selfWidth - frame.size.width - offsetX) * (1.0 - t) + offsetX : offsetX * t;
  325. button.frame = frame;
  326. offsetX += frame.size.width + (button == lastButton ? 0 : _buttonsDistance);
  327. }
  328. }
  329. -(void) transition3D:(CGFloat) t
  330. {
  331. const CGFloat invert = _fromLeft ? 1.0 : -1.0;
  332. const CGFloat angle = M_PI_2 * (1.0 - t) * invert;
  333. CATransform3D transform = CATransform3DIdentity;
  334. transform.m34 = -1.0/400.0f; //perspective 1/z
  335. const CGFloat dx = -_container.bounds.size.width * 0.5 * invert;
  336. const CGFloat offset = dx * 2 * (1.0 - t);
  337. transform = CATransform3DTranslate(transform, dx - offset, 0, 0);
  338. transform = CATransform3DRotate(transform, angle, 0.0, 1.0, 0.0);
  339. transform = CATransform3DTranslate(transform, -dx, 0, 0);
  340. _container.layer.transform = transform;
  341. }
  342. -(void) transition:(MGSwipeTransition) mode percent:(CGFloat) t
  343. {
  344. switch (mode) {
  345. case MGSwipeTransitionStatic: [self transitionStatic:t]; break;
  346. case MGSwipeTransitionDrag: [self transitionDrag:t]; break;
  347. case MGSwipeTransitionClipCenter: [self transitionClip:t]; break;
  348. case MGSwipeTransitionBorder: [self transtitionFloatBorder:t]; break;
  349. case MGSwipeTransitionRotate3D: [self transition3D:t]; break;
  350. }
  351. if (_expandedButtonAnimated && _expansionBackgroundAnimated) {
  352. _expansionBackgroundAnimated.frame = [self expansionBackgroundRect:_expandedButtonAnimated];
  353. }
  354. }
  355. @end
  356. #pragma mark Settings Classes
  357. @implementation MGSwipeSettings
  358. -(instancetype) init
  359. {
  360. if (self = [super init]) {
  361. self.transition = MGSwipeTransitionBorder;
  362. self.threshold = 0.5;
  363. self.offset = 0;
  364. self.keepButtonsSwiped = YES;
  365. self.enableSwipeBounces = YES;
  366. self.swipeBounceRate = 1.0;
  367. self.showAnimation = [[MGSwipeAnimation alloc] init];
  368. self.hideAnimation = [[MGSwipeAnimation alloc] init];
  369. self.stretchAnimation = [[MGSwipeAnimation alloc] init];
  370. }
  371. return self;
  372. }
  373. -(void) setAnimationDuration:(CGFloat)duration
  374. {
  375. _showAnimation.duration = duration;
  376. _hideAnimation.duration = duration;
  377. _stretchAnimation.duration = duration;
  378. }
  379. -(CGFloat) animationDuration {
  380. return _showAnimation.duration;
  381. }
  382. @end
  383. @implementation MGSwipeExpansionSettings
  384. -(instancetype) init
  385. {
  386. if (self = [super init]) {
  387. self.buttonIndex = -1;
  388. self.threshold = 1.3;
  389. self.animationDuration = 0.2;
  390. self.triggerAnimation = [[MGSwipeAnimation alloc] init];
  391. }
  392. return self;
  393. }
  394. @end
  395. @interface MGSwipeAnimationData : NSObject
  396. @property (nonatomic, assign) CGFloat from;
  397. @property (nonatomic, assign) CGFloat to;
  398. @property (nonatomic, assign) CFTimeInterval duration;
  399. @property (nonatomic, assign) CFTimeInterval start;
  400. @property (nonatomic, strong) MGSwipeAnimation * animation;
  401. @end
  402. @implementation MGSwipeAnimationData
  403. @end
  404. #pragma mark Easing Functions and MGSwipeAnimation
  405. static inline CGFloat mgEaseLinear(CGFloat t, CGFloat b, CGFloat c) {
  406. return c*t + b;
  407. }
  408. static inline CGFloat mgEaseInQuad(CGFloat t, CGFloat b, CGFloat c) {
  409. return c*t*t + b;
  410. }
  411. static inline CGFloat mgEaseOutQuad(CGFloat t, CGFloat b, CGFloat c) {
  412. return -c*t*(t-2) + b;
  413. }
  414. static inline CGFloat mgEaseInOutQuad(CGFloat t, CGFloat b, CGFloat c) {
  415. if ((t*=2) < 1) return c/2*t*t + b;
  416. --t;
  417. return -c/2 * (t*(t-2) - 1) + b;
  418. }
  419. static inline CGFloat mgEaseInCubic(CGFloat t, CGFloat b, CGFloat c) {
  420. return c*t*t*t + b;
  421. }
  422. static inline CGFloat mgEaseOutCubic(CGFloat t, CGFloat b, CGFloat c) {
  423. --t;
  424. return c*(t*t*t + 1) + b;
  425. }
  426. static inline CGFloat mgEaseInOutCubic(CGFloat t, CGFloat b, CGFloat c) {
  427. if ((t*=2) < 1) return c/2*t*t*t + b;
  428. t-=2;
  429. return c/2*(t*t*t + 2) + b;
  430. }
  431. static inline CGFloat mgEaseOutBounce(CGFloat t, CGFloat b, CGFloat c) {
  432. if (t < (1/2.75)) {
  433. return c*(7.5625*t*t) + b;
  434. } else if (t < (2/2.75)) {
  435. t-=(1.5/2.75);
  436. return c*(7.5625*t*t + .75) + b;
  437. } else if (t < (2.5/2.75)) {
  438. t-=(2.25/2.75);
  439. return c*(7.5625*t*t + .9375) + b;
  440. } else {
  441. t-=(2.625/2.75);
  442. return c*(7.5625*t*t + .984375) + b;
  443. }
  444. };
  445. static inline CGFloat mgEaseInBounce(CGFloat t, CGFloat b, CGFloat c) {
  446. return c - mgEaseOutBounce (1.0 -t, 0, c) + b;
  447. };
  448. static inline CGFloat mgEaseInOutBounce(CGFloat t, CGFloat b, CGFloat c) {
  449. if (t < 0.5) return mgEaseInBounce (t*2, 0, c) * .5 + b;
  450. return mgEaseOutBounce (1.0 - t*2, 0, c) * .5 + c*.5 + b;
  451. };
  452. @implementation MGSwipeAnimation
  453. -(instancetype) init {
  454. if (self = [super init]) {
  455. _duration = 0.3;
  456. _easingFunction = MGSwipeEasingFunctionCubicOut;
  457. }
  458. return self;
  459. }
  460. -(CGFloat) value:(CGFloat)elapsed duration:(CGFloat)duration from:(CGFloat)from to:(CGFloat)to
  461. {
  462. CGFloat t = MIN(elapsed/duration, 1.0f);
  463. if (t == 1.0) {
  464. return to; //precise last value
  465. }
  466. CGFloat (*easingFunction)(CGFloat t, CGFloat b, CGFloat c) = 0;
  467. switch (_easingFunction) {
  468. case MGSwipeEasingFunctionLinear: easingFunction = mgEaseLinear;break;
  469. case MGSwipeEasingFunctionQuadIn: easingFunction = mgEaseInQuad;break;
  470. case MGSwipeEasingFunctionQuadOut: easingFunction = mgEaseOutQuad;break;
  471. case MGSwipeEasingFunctionQuadInOut: easingFunction = mgEaseInOutQuad;break;
  472. case MGSwipeEasingFunctionCubicIn: easingFunction = mgEaseInCubic;break;
  473. default:
  474. case MGSwipeEasingFunctionCubicOut: easingFunction = mgEaseOutCubic;break;
  475. case MGSwipeEasingFunctionCubicInOut: easingFunction = mgEaseInOutCubic;break;
  476. case MGSwipeEasingFunctionBounceIn: easingFunction = mgEaseInBounce;break;
  477. case MGSwipeEasingFunctionBounceOut: easingFunction = mgEaseOutBounce;break;
  478. case MGSwipeEasingFunctionBounceInOut: easingFunction = mgEaseInOutBounce;break;
  479. }
  480. return (*easingFunction)(t, from, to - from);
  481. }
  482. @end
  483. #pragma mark MGSwipeTableCell Implementation
  484. @implementation MGSwipeTableCell
  485. {
  486. UITapGestureRecognizer * _tapRecognizer;
  487. UIPanGestureRecognizer * _panRecognizer;
  488. CGPoint _panStartPoint;
  489. CGFloat _panStartOffset;
  490. CGFloat _targetOffset;
  491. UIView * _swipeOverlay;
  492. UIImageView * _swipeView;
  493. UIView * _swipeContentView;
  494. MGSwipeButtonsView * _leftView;
  495. MGSwipeButtonsView * _rightView;
  496. bool _allowSwipeRightToLeft;
  497. bool _allowSwipeLeftToRight;
  498. __weak MGSwipeButtonsView * _activeExpansion;
  499. MGSwipeTableInputOverlay * _tableInputOverlay;
  500. bool _overlayEnabled;
  501. __weak UITableView * _cachedParentTable;
  502. UITableViewCellSelectionStyle _previusSelectionStyle;
  503. NSMutableSet * _previusHiddenViews;
  504. BOOL _triggerStateChanges;
  505. MGSwipeAnimationData * _animationData;
  506. void (^_animationCompletion)(BOOL finished);
  507. CADisplayLink * _displayLink;
  508. MGSwipeState _firstSwipeState;
  509. }
  510. #pragma mark View creation & layout
  511. - (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
  512. {
  513. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  514. if (self) {
  515. [self initViews:YES];
  516. }
  517. return self;
  518. }
  519. - (id)initWithCoder:(NSCoder*)aDecoder
  520. {
  521. if(self = [super initWithCoder:aDecoder]) {
  522. [self initViews:YES];
  523. }
  524. return self;
  525. }
  526. -(void) awakeFromNib
  527. {
  528. [super awakeFromNib];
  529. if (!_panRecognizer) {
  530. [self initViews:YES];
  531. }
  532. }
  533. -(void) dealloc
  534. {
  535. [self hideSwipeOverlayIfNeeded];
  536. }
  537. -(void) initViews: (BOOL) cleanButtons
  538. {
  539. if (cleanButtons) {
  540. _leftButtons = [NSArray array];
  541. _rightButtons = [NSArray array];
  542. _leftSwipeSettings = [[MGSwipeSettings alloc] init];
  543. _rightSwipeSettings = [[MGSwipeSettings alloc] init];
  544. _leftExpansion = [[MGSwipeExpansionSettings alloc] init];
  545. _rightExpansion = [[MGSwipeExpansionSettings alloc] init];
  546. }
  547. _animationData = [[MGSwipeAnimationData alloc] init];
  548. _panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panHandler:)];
  549. [self addGestureRecognizer:_panRecognizer];
  550. _panRecognizer.delegate = self;
  551. _activeExpansion = nil;
  552. _previusHiddenViews = [NSMutableSet set];
  553. _swipeState = MGSwipeStateNone;
  554. _triggerStateChanges = YES;
  555. _allowsSwipeWhenTappingButtons = YES;
  556. _preservesSelectionStatus = NO;
  557. _allowsOppositeSwipe = YES;
  558. _firstSwipeState = MGSwipeStateNone;
  559. }
  560. -(void) cleanViews
  561. {
  562. [self hideSwipeAnimated:NO];
  563. if (_displayLink) {
  564. [_displayLink invalidate];
  565. _displayLink = nil;
  566. }
  567. if (_swipeOverlay) {
  568. [_swipeOverlay removeFromSuperview];
  569. _swipeOverlay = nil;
  570. }
  571. _leftView = _rightView = nil;
  572. if (_panRecognizer) {
  573. _panRecognizer.delegate = nil;
  574. [self removeGestureRecognizer:_panRecognizer];
  575. _panRecognizer = nil;
  576. }
  577. }
  578. -(BOOL) isRTLLocale
  579. {
  580. if ([[UIView class] respondsToSelector:@selector(userInterfaceLayoutDirectionForSemanticContentAttribute:)]) {
  581. return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:self.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft;
  582. }
  583. #ifndef TARGET_IS_EXTENSION
  584. else {
  585. return [UIApplication sharedApplication].userInterfaceLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
  586. }
  587. #else
  588. return NO;
  589. #endif
  590. }
  591. -(void) fixRegionAndAccesoryViews
  592. {
  593. //Fix right to left layout direction for arabic and hebrew languagues
  594. if (self.bounds.size.width != self.contentView.bounds.size.width && [self isRTLLocale]) {
  595. _swipeOverlay.frame = CGRectMake(-self.bounds.size.width + self.contentView.bounds.size.width, 0, _swipeOverlay.bounds.size.width, _swipeOverlay.bounds.size.height);
  596. }
  597. }
  598. -(UIView *) swipeContentView
  599. {
  600. if (!_swipeContentView) {
  601. _swipeContentView = [[UIView alloc] initWithFrame:self.contentView.bounds];
  602. _swipeContentView.backgroundColor = [UIColor clearColor];
  603. _swipeContentView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  604. _swipeContentView.layer.zPosition = 9;
  605. [self.contentView addSubview:_swipeContentView];
  606. }
  607. return _swipeContentView;
  608. }
  609. -(void) layoutSubviews
  610. {
  611. [super layoutSubviews];
  612. if (_swipeContentView) {
  613. _swipeContentView.frame = self.contentView.bounds;
  614. }
  615. if (_swipeOverlay) {
  616. CGSize prevSize = _swipeView.bounds.size;
  617. _swipeOverlay.frame = CGRectMake(0, 0, self.bounds.size.width, self.contentView.bounds.size.height);
  618. [self fixRegionAndAccesoryViews];
  619. if (_swipeView.image && !CGSizeEqualToSize(prevSize, _swipeOverlay.bounds.size)) {
  620. //refresh contentView in situations like layout change, orientation chage, table resize, etc.
  621. [self refreshContentView];
  622. }
  623. }
  624. }
  625. -(void) fetchButtonsIfNeeded
  626. {
  627. if (_leftButtons.count == 0 && _delegate && [_delegate respondsToSelector:@selector(swipeTableCell:swipeButtonsForDirection:swipeSettings:expansionSettings:)]) {
  628. _leftButtons = [_delegate swipeTableCell:self swipeButtonsForDirection:MGSwipeDirectionLeftToRight swipeSettings:_leftSwipeSettings expansionSettings:_leftExpansion];
  629. }
  630. if (_rightButtons.count == 0 && _delegate && [_delegate respondsToSelector:@selector(swipeTableCell:swipeButtonsForDirection:swipeSettings:expansionSettings:)]) {
  631. _rightButtons = [_delegate swipeTableCell:self swipeButtonsForDirection:MGSwipeDirectionRightToLeft swipeSettings:_rightSwipeSettings expansionSettings:_rightExpansion];
  632. }
  633. }
  634. -(void) createSwipeViewIfNeeded
  635. {
  636. if (!_swipeOverlay) {
  637. _swipeOverlay = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, self.contentView.bounds.size.height)];
  638. [self fixRegionAndAccesoryViews];
  639. _swipeOverlay.hidden = YES;
  640. _swipeOverlay.backgroundColor = [self backgroundColorForSwipe];
  641. _swipeOverlay.layer.zPosition = 10; //force render on top of the contentView;
  642. _swipeView = [[UIImageView alloc] initWithFrame:_swipeOverlay.bounds];
  643. _swipeView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
  644. _swipeView.contentMode = UIViewContentModeCenter;
  645. _swipeView.clipsToBounds = YES;
  646. [_swipeOverlay addSubview:_swipeView];
  647. [self.contentView addSubview:_swipeOverlay];
  648. }
  649. [self fetchButtonsIfNeeded];
  650. if (!_leftView && _leftButtons.count > 0) {
  651. _leftView = [[MGSwipeButtonsView alloc] initWithButtons:_leftButtons direction:MGSwipeDirectionLeftToRight differentWidth:_allowsButtonsWithDifferentWidth buttonsDistance:_leftSwipeSettings.buttonsDistance];
  652. _leftView.cell = self;
  653. _leftView.frame = CGRectMake(-_leftView.bounds.size.width, _leftSwipeSettings.topMargin, _leftView.bounds.size.width, _swipeOverlay.bounds.size.height - _leftSwipeSettings.topMargin - _leftSwipeSettings.bottomMargin);
  654. _leftView.autoresizingMask = UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleHeight;
  655. [_swipeOverlay addSubview:_leftView];
  656. }
  657. if (!_rightView && _rightButtons.count > 0) {
  658. _rightView = [[MGSwipeButtonsView alloc] initWithButtons:_rightButtons direction:MGSwipeDirectionRightToLeft differentWidth:_allowsButtonsWithDifferentWidth buttonsDistance:_rightSwipeSettings.buttonsDistance];
  659. _rightView.cell = self;
  660. _rightView.frame = CGRectMake(_swipeOverlay.bounds.size.width, _rightSwipeSettings.topMargin, _rightView.bounds.size.width, _swipeOverlay.bounds.size.height - _rightSwipeSettings.topMargin - _rightSwipeSettings.bottomMargin);
  661. _rightView.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
  662. [_swipeOverlay addSubview:_rightView];
  663. }
  664. }
  665. - (void) showSwipeOverlayIfNeeded
  666. {
  667. if (_overlayEnabled) {
  668. return;
  669. }
  670. _overlayEnabled = YES;
  671. if (!_preservesSelectionStatus)
  672. self.selected = NO;
  673. if (_swipeContentView)
  674. [_swipeContentView removeFromSuperview];
  675. if (_delegate && [_delegate respondsToSelector:@selector(swipeTableCellWillBeginSwiping:)]) {
  676. [_delegate swipeTableCellWillBeginSwiping:self];
  677. }
  678. // snapshot cell without separator
  679. CGSize cropSize = CGSizeMake(self.bounds.size.width, self.contentView.bounds.size.height);
  680. _swipeView.image = [self imageFromView:self cropSize:cropSize];
  681. _swipeOverlay.hidden = NO;
  682. if (_swipeContentView)
  683. [_swipeView addSubview:_swipeContentView];
  684. if (!_allowsMultipleSwipe) {
  685. //input overlay on the whole table
  686. UITableView * table = [self parentTable];
  687. if (_tableInputOverlay) {
  688. [_tableInputOverlay removeFromSuperview];
  689. }
  690. _tableInputOverlay = [[MGSwipeTableInputOverlay alloc] initWithFrame:table.bounds];
  691. _tableInputOverlay.currentCell = self;
  692. [table addSubview:_tableInputOverlay];
  693. }
  694. _previusSelectionStyle = self.selectionStyle;
  695. self.selectionStyle = UITableViewCellSelectionStyleNone;
  696. [self setAccesoryViewsHidden:YES];
  697. _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHandler:)];
  698. _tapRecognizer.cancelsTouchesInView = YES;
  699. _tapRecognizer.delegate = self;
  700. [self addGestureRecognizer:_tapRecognizer];
  701. }
  702. -(void) hideSwipeOverlayIfNeeded
  703. {
  704. if (!_overlayEnabled) {
  705. return;
  706. }
  707. _overlayEnabled = NO;
  708. _swipeOverlay.hidden = YES;
  709. _swipeView.image = nil;
  710. if (_swipeContentView) {
  711. [_swipeContentView removeFromSuperview];
  712. [self.contentView addSubview:_swipeContentView];
  713. }
  714. if (_tableInputOverlay) {
  715. [_tableInputOverlay removeFromSuperview];
  716. _tableInputOverlay = nil;
  717. }
  718. self.selectionStyle = _previusSelectionStyle;
  719. NSArray * selectedRows = self.parentTable.indexPathsForSelectedRows;
  720. if ([selectedRows containsObject:[self.parentTable indexPathForCell:self]]) {
  721. self.selected = NO; //Hack: in some iOS versions setting the selected property to YES own isn't enough to force the cell to redraw the chosen selectionStyle
  722. self.selected = YES;
  723. }
  724. [self setAccesoryViewsHidden:NO];
  725. if (_delegate && [_delegate respondsToSelector:@selector(swipeTableCellWillEndSwiping:)]) {
  726. [_delegate swipeTableCellWillEndSwiping:self];
  727. }
  728. if (_tapRecognizer) {
  729. [self removeGestureRecognizer:_tapRecognizer];
  730. _tapRecognizer = nil;
  731. }
  732. }
  733. -(void) refreshContentView
  734. {
  735. CGFloat currentOffset = _swipeOffset;
  736. BOOL prevValue = _triggerStateChanges;
  737. _triggerStateChanges = NO;
  738. self.swipeOffset = 0;
  739. self.swipeOffset = currentOffset;
  740. _triggerStateChanges = prevValue;
  741. }
  742. -(void) refreshButtons: (BOOL) usingDelegate
  743. {
  744. if (usingDelegate) {
  745. self.leftButtons = @[];
  746. self.rightButtons = @[];
  747. }
  748. if (_leftView) {
  749. [_leftView removeFromSuperview];
  750. _leftView = nil;
  751. }
  752. if (_rightView) {
  753. [_rightView removeFromSuperview];
  754. _rightView = nil;
  755. }
  756. [self createSwipeViewIfNeeded];
  757. [self refreshContentView];
  758. }
  759. #pragma mark Handle Table Events
  760. -(void) willMoveToSuperview:(UIView *)newSuperview;
  761. {
  762. if (newSuperview == nil) { //remove the table overlay when a cell is removed from the table
  763. [self hideSwipeOverlayIfNeeded];
  764. }
  765. }
  766. -(void) prepareForReuse
  767. {
  768. [super prepareForReuse];
  769. [self cleanViews];
  770. if (_swipeState != MGSwipeStateNone) {
  771. _triggerStateChanges = YES;
  772. [self updateState:MGSwipeStateNone];
  773. }
  774. BOOL cleanButtons = _delegate && [_delegate respondsToSelector:@selector(swipeTableCell:swipeButtonsForDirection:swipeSettings:expansionSettings:)];
  775. [self initViews:cleanButtons];
  776. }
  777. -(void) setEditing:(BOOL)editing animated:(BOOL)animated
  778. {
  779. [super setEditing:editing animated:animated];
  780. if (editing) { //disable swipe buttons when the user sets table editing mode
  781. self.swipeOffset = 0;
  782. }
  783. }
  784. -(void) setEditing:(BOOL)editing
  785. {
  786. [super setEditing:YES];
  787. if (editing) { //disable swipe buttons when the user sets table editing mode
  788. self.swipeOffset = 0;
  789. }
  790. }
  791. -(UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event
  792. {
  793. if (!self.hidden && _swipeOverlay && !_swipeOverlay.hidden) {
  794. //override hitTest to give swipe buttons a higher priority (diclosure buttons can steal input)
  795. UIView * targets[] = {_leftView, _rightView};
  796. for (int i = 0; i< 2; ++i) {
  797. UIView * target = targets[i];
  798. if (!target) continue;
  799. CGPoint p = [self convertPoint:point toView:target];
  800. if (CGRectContainsPoint(target.bounds, p)) {
  801. return [target hitTest:p withEvent:event];
  802. }
  803. }
  804. }
  805. return [super hitTest:point withEvent:event];
  806. }
  807. #pragma mark Some utility methods
  808. - (UIImage *)imageFromView:(UIView *)view cropSize:(CGSize)cropSize{
  809. UIGraphicsBeginImageContextWithOptions(cropSize, NO, [[UIScreen mainScreen] scale]);
  810. [view.layer renderInContext:UIGraphicsGetCurrentContext()];
  811. UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
  812. UIGraphicsEndImageContext();
  813. return image;
  814. }
  815. -(void) setAccesoryViewsHidden: (BOOL) hidden
  816. {
  817. if (self.accessoryView) {
  818. self.accessoryView.hidden = hidden;
  819. }
  820. for (UIView * view in self.contentView.superview.subviews) {
  821. if (view != self.contentView && ([view isKindOfClass:[UIButton class]] || [NSStringFromClass(view.class) rangeOfString:@"Disclosure"].location != NSNotFound)) {
  822. view.hidden = hidden;
  823. }
  824. }
  825. for (UIView * view in self.contentView.subviews) {
  826. if (view == _swipeOverlay || view == _swipeContentView) continue;
  827. if (hidden && !view.hidden) {
  828. view.hidden = YES;
  829. [_previusHiddenViews addObject:view];
  830. }
  831. else if (!hidden && [_previusHiddenViews containsObject:view]) {
  832. view.hidden = NO;
  833. }
  834. }
  835. if (!hidden) {
  836. [_previusHiddenViews removeAllObjects];
  837. }
  838. }
  839. -(UIColor *) backgroundColorForSwipe
  840. {
  841. if (_swipeBackgroundColor) {
  842. return _swipeBackgroundColor; //user defined color
  843. }
  844. else if (self.contentView.backgroundColor && ![self.contentView.backgroundColor isEqual:[UIColor clearColor]]) {
  845. return self.contentView.backgroundColor;
  846. }
  847. else if (self.backgroundColor) {
  848. return self.backgroundColor;
  849. }
  850. return [UIColor clearColor];
  851. }
  852. -(UITableView *) parentTable
  853. {
  854. if (_cachedParentTable) {
  855. return _cachedParentTable;
  856. }
  857. UIView * view = self.superview;
  858. while(view != nil) {
  859. if([view isKindOfClass:[UITableView class]]) {
  860. _cachedParentTable = (UITableView*) view;
  861. }
  862. view = view.superview;
  863. }
  864. return _cachedParentTable;
  865. }
  866. -(void) updateState: (MGSwipeState) newState;
  867. {
  868. if (!_triggerStateChanges || _swipeState == newState) {
  869. return;
  870. }
  871. _swipeState = newState;
  872. if (_delegate && [_delegate respondsToSelector:@selector(swipeTableCell:didChangeSwipeState:gestureIsActive:)]) {
  873. [_delegate swipeTableCell:self didChangeSwipeState:_swipeState gestureIsActive: self.isSwipeGestureActive] ;
  874. }
  875. }
  876. #pragma mark Swipe Animation
  877. - (void)setSwipeOffset:(CGFloat) newOffset;
  878. {
  879. CGFloat sign = newOffset > 0 ? 1.0 : -1.0;
  880. MGSwipeButtonsView * activeButtons = sign < 0 ? _rightView : _leftView;
  881. MGSwipeSettings * activeSettings = sign < 0 ? _rightSwipeSettings : _leftSwipeSettings;
  882. if(activeSettings.enableSwipeBounces) {
  883. _swipeOffset = newOffset;
  884. CGFloat maxUnbouncedOffset = sign * activeButtons.bounds.size.width;
  885. if ((sign > 0 && newOffset > maxUnbouncedOffset) || (sign < 0 && newOffset < maxUnbouncedOffset)) {
  886. _swipeOffset = maxUnbouncedOffset + (newOffset - maxUnbouncedOffset) * activeSettings.swipeBounceRate;
  887. }
  888. }
  889. else {
  890. CGFloat maxOffset = sign * activeButtons.bounds.size.width;
  891. _swipeOffset = sign > 0 ? MIN(newOffset, maxOffset) : MAX(newOffset, maxOffset);
  892. }
  893. CGFloat offset = fabs(_swipeOffset);
  894. if (!activeButtons || offset == 0) {
  895. if (_leftView)
  896. [_leftView endExpansionAnimated:NO];
  897. if (_rightView)
  898. [_rightView endExpansionAnimated:NO];
  899. [self hideSwipeOverlayIfNeeded];
  900. _targetOffset = 0;
  901. [self updateState:MGSwipeStateNone];
  902. return;
  903. }
  904. else {
  905. [self showSwipeOverlayIfNeeded];
  906. CGFloat swipeThreshold = activeSettings.threshold;
  907. BOOL keepButtons = activeSettings.keepButtonsSwiped;
  908. _targetOffset = keepButtons && offset > activeButtons.bounds.size.width * swipeThreshold ? activeButtons.bounds.size.width * sign : 0;
  909. }
  910. BOOL onlyButtons = activeSettings.onlySwipeButtons;
  911. _swipeView.transform = CGAffineTransformMakeTranslation(onlyButtons ? 0 : _swipeOffset, 0);
  912. //animate existing buttons
  913. MGSwipeButtonsView* but[2] = {_leftView, _rightView};
  914. MGSwipeSettings* settings[2] = {_leftSwipeSettings, _rightSwipeSettings};
  915. MGSwipeExpansionSettings * expansions[2] = {_leftExpansion, _rightExpansion};
  916. for (int i = 0; i< 2; ++i) {
  917. MGSwipeButtonsView * view = but[i];
  918. if (!view) continue;
  919. //buttons view position
  920. CGFloat translation = MIN(offset, view.bounds.size.width) * sign + settings[i].offset * sign;
  921. view.transform = CGAffineTransformMakeTranslation(translation, 0);
  922. if (view != activeButtons) continue; //only transition if active (perf. improvement)
  923. bool expand = expansions[i].buttonIndex >= 0 && offset > view.bounds.size.width * expansions[i].threshold;
  924. if (expand) {
  925. [view expandToOffset:offset settings:expansions[i]];
  926. _targetOffset = expansions[i].fillOnTrigger ? self.bounds.size.width * sign : 0;
  927. _activeExpansion = view;
  928. [self updateState:i ? MGSwipeStateExpandingRightToLeft : MGSwipeStateExpandingLeftToRight];
  929. }
  930. else {
  931. [view endExpansionAnimated:YES];
  932. _activeExpansion = nil;
  933. CGFloat t = MIN(1.0f, offset/view.bounds.size.width);
  934. [view transition:settings[i].transition percent:t];
  935. [self updateState:i ? MGSwipeStateSwipingRightToLeft : MGSwipeStateSwipingLeftToRight];
  936. }
  937. }
  938. }
  939. -(void) hideSwipeAnimated: (BOOL) animated completion:(void(^)(BOOL finished)) completion
  940. {
  941. MGSwipeAnimation * animation = animated ? (_swipeOffset > 0 ? _leftSwipeSettings.hideAnimation: _rightSwipeSettings.hideAnimation) : nil;
  942. [self setSwipeOffset:0 animation:animation completion:completion];
  943. }
  944. -(void) hideSwipeAnimated: (BOOL) animated
  945. {
  946. [self hideSwipeAnimated:animated completion:nil];
  947. }
  948. -(void) showSwipe: (MGSwipeDirection) direction animated: (BOOL) animated
  949. {
  950. [self showSwipe:direction animated:animated completion:nil];
  951. }
  952. -(void) showSwipe: (MGSwipeDirection) direction animated: (BOOL) animated completion:(void(^)(BOOL finished)) completion
  953. {
  954. [self createSwipeViewIfNeeded];
  955. _allowSwipeLeftToRight = _leftButtons.count > 0;
  956. _allowSwipeRightToLeft = _rightButtons.count > 0;
  957. UIView * buttonsView = direction == MGSwipeDirectionLeftToRight ? _leftView : _rightView;
  958. if (buttonsView) {
  959. CGFloat s = direction == MGSwipeDirectionLeftToRight ? 1.0 : -1.0;
  960. MGSwipeAnimation * animation = animated ? (direction == MGSwipeDirectionLeftToRight ? _leftSwipeSettings.showAnimation : _rightSwipeSettings.showAnimation) : nil;
  961. [self setSwipeOffset:buttonsView.bounds.size.width * s animation:animation completion:completion];
  962. }
  963. }
  964. -(void) expandSwipe: (MGSwipeDirection) direction animated: (BOOL) animated
  965. {
  966. CGFloat s = direction == MGSwipeDirectionLeftToRight ? 1.0 : -1.0;
  967. MGSwipeExpansionSettings* expSetting = direction == MGSwipeDirectionLeftToRight ? _leftExpansion : _rightExpansion;
  968. // only perform animation if there's no pending expansion animation and requested direction has fillOnTrigger enabled
  969. if(!_activeExpansion && expSetting.fillOnTrigger) {
  970. [self createSwipeViewIfNeeded];
  971. _allowSwipeLeftToRight = _leftButtons.count > 0;
  972. _allowSwipeRightToLeft = _rightButtons.count > 0;
  973. UIView * buttonsView = direction == MGSwipeDirectionLeftToRight ? _leftView : _rightView;
  974. if (buttonsView) {
  975. __weak MGSwipeButtonsView * expansionView = direction == MGSwipeDirectionLeftToRight ? _leftView : _rightView;
  976. __weak MGSwipeTableCell * weakself = self;
  977. [self setSwipeOffset:buttonsView.bounds.size.width * s * expSetting.threshold * 2 animation:expSetting.triggerAnimation completion:^(BOOL finished){
  978. [expansionView endExpansionAnimated:YES];
  979. [weakself setSwipeOffset:0 animated:NO completion:nil];
  980. }];
  981. }
  982. }
  983. }
  984. -(void) animationTick: (CADisplayLink *) timer
  985. {
  986. if (!_animationData.start) {
  987. _animationData.start = timer.timestamp;
  988. }
  989. CFTimeInterval elapsed = timer.timestamp - _animationData.start;
  990. bool completed = elapsed >= _animationData.duration;
  991. if (completed) {
  992. _triggerStateChanges = YES;
  993. }
  994. self.swipeOffset = [_animationData.animation value:elapsed duration:_animationData.duration from:_animationData.from to:_animationData.to];
  995. //call animation completion and invalidate timer
  996. if (completed){
  997. [timer invalidate];
  998. [self invalidateDisplayLink];
  999. }
  1000. }
  1001. -(void)invalidateDisplayLink {
  1002. [_displayLink invalidate];
  1003. _displayLink = nil;
  1004. if (_animationCompletion) {
  1005. void (^callbackCopy)(BOOL finished) = _animationCompletion; //copy to avoid duplicated callbacks
  1006. _animationCompletion = nil;
  1007. callbackCopy(YES);
  1008. }
  1009. }
  1010. -(void) setSwipeOffset:(CGFloat)offset animated: (BOOL) animated completion:(void(^)(BOOL finished)) completion
  1011. {
  1012. MGSwipeAnimation * animation = animated ? [[MGSwipeAnimation alloc] init] : nil;
  1013. [self setSwipeOffset:offset animation:animation completion:completion];
  1014. }
  1015. -(void) setSwipeOffset:(CGFloat)offset animation: (MGSwipeAnimation *) animation completion:(void(^)(BOOL finished)) completion
  1016. {
  1017. if (_displayLink) {
  1018. [_displayLink invalidate];
  1019. _displayLink = nil;
  1020. }
  1021. if (_animationCompletion) { //notify previous animation cancelled
  1022. void (^callbackCopy)(BOOL finished) = _animationCompletion; //copy to avoid duplicated callbacks
  1023. _animationCompletion = nil;
  1024. callbackCopy(NO);
  1025. }
  1026. if (offset !=0) {
  1027. [self createSwipeViewIfNeeded];
  1028. }
  1029. if (!animation) {
  1030. self.swipeOffset = offset;
  1031. if (completion) {
  1032. completion(YES);
  1033. }
  1034. return;
  1035. }
  1036. _animationCompletion = completion;
  1037. _triggerStateChanges = NO;
  1038. _animationData.from = _swipeOffset;
  1039. _animationData.to = offset;
  1040. _animationData.duration = animation.duration;
  1041. _animationData.start = 0;
  1042. _animationData.animation = animation;
  1043. _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(animationTick:)];
  1044. [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
  1045. }
  1046. #pragma mark Gestures
  1047. -(void) cancelPanGesture
  1048. {
  1049. if (_panRecognizer.state != UIGestureRecognizerStateEnded && _panRecognizer.state != UIGestureRecognizerStatePossible) {
  1050. _panRecognizer.enabled = NO;
  1051. _panRecognizer.enabled = YES;
  1052. if (self.swipeOffset) {
  1053. [self hideSwipeAnimated:YES];
  1054. }
  1055. }
  1056. }
  1057. -(void) tapHandler: (UITapGestureRecognizer *) recognizer
  1058. {
  1059. BOOL hide = YES;
  1060. if (_delegate && [_delegate respondsToSelector:@selector(swipeTableCell:shouldHideSwipeOnTap:)]) {
  1061. hide = [_delegate swipeTableCell:self shouldHideSwipeOnTap:[recognizer locationInView:self]];
  1062. }
  1063. if (hide) {
  1064. [self hideSwipeAnimated:YES];
  1065. }
  1066. }
  1067. -(CGFloat) filterSwipe: (CGFloat) offset
  1068. {
  1069. bool allowed = offset > 0 ? _allowSwipeLeftToRight : _allowSwipeRightToLeft;
  1070. UIView * buttons = offset > 0 ? _leftView : _rightView;
  1071. if (!buttons || ! allowed) {
  1072. offset = 0;
  1073. }
  1074. else if (!_allowsOppositeSwipe && _firstSwipeState == MGSwipeStateSwipingLeftToRight && offset < 0) {
  1075. offset = 0;
  1076. }
  1077. else if (!_allowsOppositeSwipe && _firstSwipeState == MGSwipeStateSwipingRightToLeft && offset > 0 ) {
  1078. offset = 0;
  1079. }
  1080. return offset;
  1081. }
  1082. -(void) panHandler: (UIPanGestureRecognizer *)gesture
  1083. {
  1084. CGPoint current = [gesture translationInView:self];
  1085. if (gesture.state == UIGestureRecognizerStateBegan) {
  1086. [self invalidateDisplayLink];
  1087. if (!_preservesSelectionStatus)
  1088. self.highlighted = NO;
  1089. [self createSwipeViewIfNeeded];
  1090. _panStartPoint = current;
  1091. _panStartOffset = _swipeOffset;
  1092. if (_swipeOffset != 0) {
  1093. _firstSwipeState = _swipeOffset > 0 ? MGSwipeStateSwipingLeftToRight : MGSwipeStateSwipingRightToLeft;
  1094. }
  1095. if (!_allowsMultipleSwipe) {
  1096. NSArray * cells = [self parentTable].visibleCells;
  1097. for (MGSwipeTableCell * cell in cells) {
  1098. if ([cell isKindOfClass:[MGSwipeTableCell class]] && cell != self) {
  1099. [cell cancelPanGesture];
  1100. }
  1101. }
  1102. }
  1103. }
  1104. else if (gesture.state == UIGestureRecognizerStateChanged) {
  1105. CGFloat offset = _panStartOffset + current.x - _panStartPoint.x;
  1106. if (_firstSwipeState == MGSwipeStateNone) {
  1107. _firstSwipeState = offset > 0 ? MGSwipeStateSwipingLeftToRight : MGSwipeStateSwipingRightToLeft;
  1108. }
  1109. self.swipeOffset = [self filterSwipe:offset];
  1110. }
  1111. else {
  1112. __weak MGSwipeButtonsView * expansion = _activeExpansion;
  1113. if (expansion) {
  1114. __weak UIView * expandedButton = [expansion getExpandedButton];
  1115. MGSwipeExpansionSettings * expSettings = _swipeOffset > 0 ? _leftExpansion : _rightExpansion;
  1116. UIColor * backgroundColor = nil;
  1117. if (!expSettings.fillOnTrigger && expSettings.expansionColor) {
  1118. backgroundColor = expansion.backgroundColorCopy; //keep expansion background color
  1119. expansion.backgroundColorCopy = expSettings.expansionColor;
  1120. }
  1121. [self setSwipeOffset:_targetOffset animation:expSettings.triggerAnimation completion:^(BOOL finished){
  1122. if (!finished || self.hidden || !expansion) {
  1123. return; //cell might be hidden after a delete row animation without being deallocated (to be reused later)
  1124. }
  1125. BOOL autoHide = [expansion handleClick:expandedButton fromExpansion:YES];
  1126. if (autoHide) {
  1127. [expansion endExpansionAnimated:NO];
  1128. }
  1129. if (backgroundColor && expandedButton) {
  1130. expandedButton.backgroundColor = backgroundColor;
  1131. }
  1132. }];
  1133. }
  1134. else {
  1135. CGFloat velocity = [_panRecognizer velocityInView:self].x;
  1136. CGFloat inertiaThreshold = 100.0; //points per second
  1137. if (velocity > inertiaThreshold) {
  1138. _targetOffset = _swipeOffset < 0 ? 0 : (_leftView && _leftSwipeSettings.keepButtonsSwiped ? _leftView.bounds.size.width : _targetOffset);
  1139. }
  1140. else if (velocity < -inertiaThreshold) {
  1141. _targetOffset = _swipeOffset > 0 ? 0 : (_rightView && _rightSwipeSettings.keepButtonsSwiped ? -_rightView.bounds.size.width : _targetOffset);
  1142. }
  1143. _targetOffset = [self filterSwipe:_targetOffset];
  1144. MGSwipeSettings * settings = _swipeOffset > 0 ? _leftSwipeSettings : _rightSwipeSettings;
  1145. MGSwipeAnimation * animation = nil;
  1146. if (_targetOffset == 0) {
  1147. animation = settings.hideAnimation;
  1148. }
  1149. else if (fabs(_swipeOffset) > fabs(_targetOffset)) {
  1150. animation = settings.stretchAnimation;
  1151. }
  1152. else {
  1153. animation = settings.showAnimation;
  1154. }
  1155. [self setSwipeOffset:_targetOffset animation:animation completion:nil];
  1156. }
  1157. _firstSwipeState = MGSwipeStateNone;
  1158. }
  1159. }
  1160. - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
  1161. if (gestureRecognizer == _panRecognizer) {
  1162. if (self.isEditing) {
  1163. return NO; //do not swipe while editing table
  1164. }
  1165. CGPoint translation = [_panRecognizer translationInView:self];
  1166. if (fabs(translation.y) > fabs(translation.x)) {
  1167. return NO; // user is scrolling vertically
  1168. }
  1169. if (_swipeView) {
  1170. CGPoint point = [_tapRecognizer locationInView:_swipeView];
  1171. if (!CGRectContainsPoint(_swipeView.bounds, point)) {
  1172. return _allowsSwipeWhenTappingButtons; //user clicked outside the cell or in the buttons area
  1173. }
  1174. }
  1175. if (_swipeOffset != 0.0) {
  1176. return YES; //already swiped, don't need to check buttons or canSwipe delegate
  1177. }
  1178. //make a decision according to existing buttons or using the optional delegate
  1179. if (_delegate && [_delegate respondsToSelector:@selector(swipeTableCell:canSwipe:fromPoint:)]) {
  1180. CGPoint point = [_panRecognizer locationInView:self];
  1181. _allowSwipeLeftToRight = [_delegate swipeTableCell:self canSwipe:MGSwipeDirectionLeftToRight fromPoint:point];
  1182. _allowSwipeRightToLeft = [_delegate swipeTableCell:self canSwipe:MGSwipeDirectionRightToLeft fromPoint:point];
  1183. }
  1184. else if (_delegate && [_delegate respondsToSelector:@selector(swipeTableCell:canSwipe:)]) {
  1185. #pragma clang diagnostic push
  1186. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  1187. _allowSwipeLeftToRight = [_delegate swipeTableCell:self canSwipe:MGSwipeDirectionLeftToRight];
  1188. _allowSwipeRightToLeft = [_delegate swipeTableCell:self canSwipe:MGSwipeDirectionRightToLeft];
  1189. #pragma clang diagnostic pop
  1190. }
  1191. else {
  1192. [self fetchButtonsIfNeeded];
  1193. _allowSwipeLeftToRight = _leftButtons.count > 0;
  1194. _allowSwipeRightToLeft = _rightButtons.count > 0;
  1195. }
  1196. return (_allowSwipeLeftToRight && translation.x > 0) || (_allowSwipeRightToLeft && translation.x < 0);
  1197. }
  1198. else if (gestureRecognizer == _tapRecognizer) {
  1199. CGPoint point = [_tapRecognizer locationInView:_swipeView];
  1200. return CGRectContainsPoint(_swipeView.bounds, point);
  1201. }
  1202. return YES;
  1203. }
  1204. -(BOOL) isSwipeGestureActive
  1205. {
  1206. return _panRecognizer.state == UIGestureRecognizerStateBegan || _panRecognizer.state == UIGestureRecognizerStateChanged;
  1207. }
  1208. -(void)setSwipeBackgroundColor:(UIColor *)swipeBackgroundColor {
  1209. _swipeBackgroundColor = swipeBackgroundColor;
  1210. if (_swipeOverlay) {
  1211. _swipeOverlay.backgroundColor = swipeBackgroundColor;
  1212. }
  1213. }
  1214. #pragma mark Accessibility
  1215. - (NSInteger)accessibilityElementCount {
  1216. return _swipeOffset == 0 ? [super accessibilityElementCount] : 1;
  1217. }
  1218. - (id)accessibilityElementAtIndex:(NSInteger)index {
  1219. return _swipeOffset == 0 ? [super accessibilityElementAtIndex:index] : self.contentView;
  1220. }
  1221. - (NSInteger)indexOfAccessibilityElement:(id)element {
  1222. return _swipeOffset == 0 ? [super indexOfAccessibilityElement:element] : 0;
  1223. }
  1224. @end