MGSwipeTableCell.m 52 KB

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