SWTableViewCell.m 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766
  1. //
  2. // SWTableViewCell.m
  3. // SWTableViewCell
  4. //
  5. // Created by Chris Wendel on 9/10/13.
  6. // Copyright (c) 2013 Chris Wendel. All rights reserved.
  7. //
  8. #import "SWTableViewCell.h"
  9. #import "SWUtilityButtonView.h"
  10. static NSString * const kTableViewCellContentView = @"UITableViewCellContentView";
  11. #define kSectionIndexWidth 15
  12. #define kAccessoryTrailingSpace 15
  13. #define kLongPressMinimumDuration 0.16f
  14. @interface SWTableViewCell () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
  15. @property (nonatomic, weak) UITableView *containingTableView;
  16. @property (nonatomic, strong) UIPanGestureRecognizer *tableViewPanGestureRecognizer;
  17. @property (nonatomic, assign) SWCellState cellState; // The state of the cell within the scroll view, can be left, right or middle
  18. @property (nonatomic, assign) CGFloat additionalRightPadding;
  19. @property (nonatomic, strong) UIScrollView *cellScrollView;
  20. @property (nonatomic, strong) SWUtilityButtonView *leftUtilityButtonsView, *rightUtilityButtonsView;
  21. @property (nonatomic, strong) UIView *leftUtilityClipView, *rightUtilityClipView;
  22. @property (nonatomic, strong) NSLayoutConstraint *leftUtilityClipConstraint, *rightUtilityClipConstraint;
  23. @property (nonatomic, strong) UILongPressGestureRecognizer *longPressGestureRecognizer;
  24. @property (nonatomic, strong) UITapGestureRecognizer *tapGestureRecognizer;
  25. - (CGFloat)leftUtilityButtonsWidth;
  26. - (CGFloat)rightUtilityButtonsWidth;
  27. - (CGFloat)utilityButtonsPadding;
  28. - (CGPoint)contentOffsetForCellState:(SWCellState)state;
  29. - (void)updateCellState;
  30. - (BOOL)shouldHighlight;
  31. @end
  32. @implementation SWTableViewCell {
  33. UIView *_contentCellView;
  34. }
  35. #pragma mark Initializers
  36. - (instancetype)initWithCoder:(NSCoder *)aDecoder
  37. {
  38. self = [super initWithCoder:aDecoder];
  39. if (self)
  40. {
  41. [self initializer];
  42. }
  43. return self;
  44. }
  45. - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
  46. {
  47. self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
  48. if (self)
  49. {
  50. [self initializer];
  51. }
  52. return self;
  53. }
  54. - (void)initializer
  55. {
  56. // Set up scroll view that will host our cell content
  57. self.cellScrollView = [[SWCellScrollView alloc] init];
  58. self.cellScrollView.translatesAutoresizingMaskIntoConstraints = NO;
  59. self.cellScrollView.delegate = self;
  60. self.cellScrollView.showsHorizontalScrollIndicator = NO;
  61. self.cellScrollView.scrollsToTop = NO;
  62. self.cellScrollView.scrollEnabled = YES;
  63. _contentCellView = [[UIView alloc] init];
  64. [self.cellScrollView addSubview:_contentCellView];
  65. // Add the cell scroll view to the cell
  66. UIView *contentViewParent = self;
  67. UIView *clipViewParent = self.cellScrollView;
  68. if (![NSStringFromClass([[self.subviews objectAtIndex:0] class]) isEqualToString:kTableViewCellContentView])
  69. {
  70. // iOS 7
  71. contentViewParent = [self.subviews objectAtIndex:0];
  72. clipViewParent = self;
  73. }
  74. NSArray *cellSubviews = [contentViewParent subviews];
  75. [self insertSubview:self.cellScrollView atIndex:0];
  76. for (UIView *subview in cellSubviews)
  77. {
  78. [_contentCellView addSubview:subview];
  79. }
  80. // Set scroll view to perpetually have same frame as self. Specifying relative to superview doesn't work, since the latter UITableViewCellScrollView has different behaviour.
  81. [self addConstraints:@[
  82. [NSLayoutConstraint constraintWithItem:self.cellScrollView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0],
  83. [NSLayoutConstraint constraintWithItem:self.cellScrollView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0],
  84. [NSLayoutConstraint constraintWithItem:self.cellScrollView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0],
  85. [NSLayoutConstraint constraintWithItem:self.cellScrollView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0],
  86. ]];
  87. self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewTapped:)];
  88. self.tapGestureRecognizer.cancelsTouchesInView = NO;
  89. self.tapGestureRecognizer.delegate = self;
  90. [self.cellScrollView addGestureRecognizer:self.tapGestureRecognizer];
  91. self.longPressGestureRecognizer = [[SWLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(scrollViewPressed:)];
  92. self.longPressGestureRecognizer.cancelsTouchesInView = NO;
  93. self.longPressGestureRecognizer.minimumPressDuration = kLongPressMinimumDuration;
  94. self.longPressGestureRecognizer.delegate = self;
  95. [self.cellScrollView addGestureRecognizer:self.longPressGestureRecognizer];
  96. // Create the left and right utility button views, as well as vanilla UIViews in which to embed them. We can manipulate the latter in order to effect clipping according to scroll position.
  97. // Such an approach is necessary in order for the utility views to sit on top to get taps, as well as allow the backgroundColor (and private UITableViewCellBackgroundView) to work properly.
  98. self.leftUtilityClipView = [[UIView alloc] init];
  99. self.leftUtilityClipConstraint = [NSLayoutConstraint constraintWithItem:self.leftUtilityClipView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeLeft multiplier:1.0 constant:0.0];
  100. self.leftUtilityButtonsView = [[SWUtilityButtonView alloc] initWithUtilityButtons:nil
  101. parentCell:self
  102. utilityButtonSelector:@selector(leftUtilityButtonHandler:)];
  103. self.rightUtilityClipView = [[UIView alloc] initWithFrame:self.bounds];
  104. self.rightUtilityClipConstraint = [NSLayoutConstraint constraintWithItem:self.rightUtilityClipView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeRight multiplier:1.0 constant:0.0];
  105. self.rightUtilityButtonsView = [[SWUtilityButtonView alloc] initWithUtilityButtons:nil
  106. parentCell:self
  107. utilityButtonSelector:@selector(rightUtilityButtonHandler:)];
  108. UIView *clipViews[] = { self.rightUtilityClipView, self.leftUtilityClipView };
  109. NSLayoutConstraint *clipConstraints[] = { self.rightUtilityClipConstraint, self.leftUtilityClipConstraint };
  110. UIView *buttonViews[] = { self.rightUtilityButtonsView, self.leftUtilityButtonsView };
  111. NSLayoutAttribute alignmentAttributes[] = { NSLayoutAttributeRight, NSLayoutAttributeLeft };
  112. for (NSUInteger i = 0; i < 2; ++i)
  113. {
  114. UIView *clipView = clipViews[i];
  115. NSLayoutConstraint *clipConstraint = clipConstraints[i];
  116. UIView *buttonView = buttonViews[i];
  117. NSLayoutAttribute alignmentAttribute = alignmentAttributes[i];
  118. clipConstraint.priority = UILayoutPriorityDefaultHigh;
  119. clipView.translatesAutoresizingMaskIntoConstraints = NO;
  120. clipView.clipsToBounds = YES;
  121. [clipViewParent addSubview:clipView];
  122. [self addConstraints:@[
  123. // Pin the clipping view to the appropriate outer edges of the cell.
  124. [NSLayoutConstraint constraintWithItem:clipView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0],
  125. [NSLayoutConstraint constraintWithItem:clipView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0],
  126. [NSLayoutConstraint constraintWithItem:clipView attribute:alignmentAttribute relatedBy:NSLayoutRelationEqual toItem:self attribute:alignmentAttribute multiplier:1.0 constant:0.0],
  127. clipConstraint,
  128. ]];
  129. [clipView addSubview:buttonView];
  130. [self addConstraints:@[
  131. // Pin the button view to the appropriate outer edges of its clipping view.
  132. [NSLayoutConstraint constraintWithItem:buttonView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:clipView attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0],
  133. [NSLayoutConstraint constraintWithItem:buttonView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:clipView attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0],
  134. [NSLayoutConstraint constraintWithItem:buttonView attribute:alignmentAttribute relatedBy:NSLayoutRelationEqual toItem:clipView attribute:alignmentAttribute multiplier:1.0 constant:0.0],
  135. // Constrain the maximum button width so that at least a button's worth of contentView is left visible. (The button view will shrink accordingly.)
  136. [NSLayoutConstraint constraintWithItem:buttonView attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationLessThanOrEqual toItem:self.contentView attribute:NSLayoutAttributeWidth multiplier:1.0 constant:-kUtilityButtonWidthDefault],
  137. ]];
  138. }
  139. }
  140. static NSString * const kTableViewPanState = @"state";
  141. - (void)removeOldTableViewPanObserver
  142. {
  143. [_tableViewPanGestureRecognizer removeObserver:self forKeyPath:kTableViewPanState];
  144. }
  145. - (void)dealloc
  146. {
  147. [self removeOldTableViewPanObserver];
  148. }
  149. - (void)setContainingTableView:(UITableView *)containingTableView
  150. {
  151. [self removeOldTableViewPanObserver];
  152. _tableViewPanGestureRecognizer = containingTableView.panGestureRecognizer;
  153. _containingTableView = containingTableView;
  154. if (containingTableView)
  155. {
  156. // Check if the UITableView will display Indices on the right. If that's the case, add a padding
  157. if ([_containingTableView.dataSource respondsToSelector:@selector(sectionIndexTitlesForTableView:)])
  158. {
  159. NSArray *indices = [_containingTableView.dataSource sectionIndexTitlesForTableView:_containingTableView];
  160. self.additionalRightPadding = indices == nil ? 0 : kSectionIndexWidth;
  161. }
  162. _containingTableView.directionalLockEnabled = YES;
  163. [self.tapGestureRecognizer requireGestureRecognizerToFail:_containingTableView.panGestureRecognizer];
  164. [_tableViewPanGestureRecognizer addObserver:self forKeyPath:kTableViewPanState options:0 context:nil];
  165. }
  166. }
  167. - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  168. {
  169. if([keyPath isEqualToString:kTableViewPanState] && object == _tableViewPanGestureRecognizer)
  170. {
  171. if(_tableViewPanGestureRecognizer.state == UIGestureRecognizerStateBegan)
  172. {
  173. CGPoint locationInTableView = [_tableViewPanGestureRecognizer locationInView:_containingTableView];
  174. BOOL inCurrentCell = CGRectContainsPoint(self.frame, locationInTableView);
  175. if(!inCurrentCell && _cellState != kCellStateCenter)
  176. {
  177. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:)])
  178. {
  179. if([self.delegate swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:self])
  180. {
  181. [self hideUtilityButtonsAnimated:YES];
  182. }
  183. }
  184. }
  185. }
  186. }
  187. }
  188. - (void)setLeftUtilityButtons:(NSArray *)leftUtilityButtons
  189. {
  190. if (![_leftUtilityButtons sw_isEqualToButtons:leftUtilityButtons]) {
  191. _leftUtilityButtons = leftUtilityButtons;
  192. self.leftUtilityButtonsView.utilityButtons = leftUtilityButtons;
  193. [self.leftUtilityButtonsView layoutIfNeeded];
  194. [self layoutIfNeeded];
  195. }
  196. }
  197. - (void)setLeftUtilityButtons:(NSArray *)leftUtilityButtons WithButtonWidth:(CGFloat) width
  198. {
  199. _leftUtilityButtons = leftUtilityButtons;
  200. [self.leftUtilityButtonsView setUtilityButtons:leftUtilityButtons WithButtonWidth:width];
  201. [self.leftUtilityButtonsView layoutIfNeeded];
  202. [self layoutIfNeeded];
  203. }
  204. - (void)setRightUtilityButtons:(NSArray *)rightUtilityButtons
  205. {
  206. if (![_rightUtilityButtons sw_isEqualToButtons:rightUtilityButtons]) {
  207. _rightUtilityButtons = rightUtilityButtons;
  208. self.rightUtilityButtonsView.utilityButtons = rightUtilityButtons;
  209. [self.rightUtilityButtonsView layoutIfNeeded];
  210. [self layoutIfNeeded];
  211. }
  212. }
  213. - (void)setRightUtilityButtons:(NSArray *)rightUtilityButtons WithButtonWidth:(CGFloat) width
  214. {
  215. _rightUtilityButtons = rightUtilityButtons;
  216. [self.rightUtilityButtonsView setUtilityButtons:rightUtilityButtons WithButtonWidth:width];
  217. [self.rightUtilityButtonsView layoutIfNeeded];
  218. [self layoutIfNeeded];
  219. }
  220. #pragma mark - UITableViewCell overrides
  221. - (void)didMoveToSuperview
  222. {
  223. self.containingTableView = nil;
  224. UIView *view = self.superview;
  225. do {
  226. if ([view isKindOfClass:[UITableView class]])
  227. {
  228. self.containingTableView = (UITableView *)view;
  229. break;
  230. }
  231. } while ((view = view.superview));
  232. }
  233. - (void)layoutSubviews
  234. {
  235. [super layoutSubviews];
  236. // Offset the contentView origin so that it appears correctly w/rt the enclosing scroll view (to which we moved it).
  237. CGRect frame = self.contentView.frame;
  238. frame.origin.x = [self leftUtilityButtonsWidth];
  239. _contentCellView.frame = frame;
  240. self.cellScrollView.contentSize = CGSizeMake(CGRectGetWidth(self.frame) + [self utilityButtonsPadding], CGRectGetHeight(self.frame));
  241. if (!self.cellScrollView.isTracking && !self.cellScrollView.isDecelerating)
  242. {
  243. self.cellScrollView.contentOffset = [self contentOffsetForCellState:_cellState];
  244. }
  245. [self updateCellState];
  246. }
  247. - (void)prepareForReuse
  248. {
  249. [super prepareForReuse];
  250. [self hideUtilityButtonsAnimated:NO];
  251. }
  252. - (void)setSelected:(BOOL)selected animated:(BOOL)animated
  253. {
  254. // Work around stupid background-destroying override magic that UITableView seems to perform on contained buttons.
  255. [self.leftUtilityButtonsView pushBackgroundColors];
  256. [self.rightUtilityButtonsView pushBackgroundColors];
  257. [super setSelected:selected animated:animated];
  258. [self.leftUtilityButtonsView popBackgroundColors];
  259. [self.rightUtilityButtonsView popBackgroundColors];
  260. }
  261. - (void)didTransitionToState:(UITableViewCellStateMask)state {
  262. [super didTransitionToState:state];
  263. if (state == UITableViewCellStateDefaultMask) {
  264. [self layoutSubviews];
  265. }
  266. }
  267. #pragma mark - Selection handling
  268. - (BOOL)shouldHighlight
  269. {
  270. BOOL shouldHighlight = YES;
  271. if ([self.containingTableView.delegate respondsToSelector:@selector(tableView:shouldHighlightRowAtIndexPath:)])
  272. {
  273. NSIndexPath *cellIndexPath = [self.containingTableView indexPathForCell:self];
  274. shouldHighlight = [self.containingTableView.delegate tableView:self.containingTableView shouldHighlightRowAtIndexPath:cellIndexPath];
  275. }
  276. return shouldHighlight;
  277. }
  278. - (void)scrollViewPressed:(UIGestureRecognizer *)gestureRecognizer
  279. {
  280. if (gestureRecognizer.state == UIGestureRecognizerStateBegan && !self.isHighlighted && self.shouldHighlight)
  281. {
  282. [self setHighlighted:YES animated:NO];
  283. }
  284. else if (gestureRecognizer.state == UIGestureRecognizerStateEnded)
  285. {
  286. // Cell is already highlighted; clearing it temporarily seems to address visual anomaly.
  287. [self setHighlighted:NO animated:NO];
  288. [self scrollViewTapped:gestureRecognizer];
  289. }
  290. else if (gestureRecognizer.state == UIGestureRecognizerStateCancelled)
  291. {
  292. [self setHighlighted:NO animated:NO];
  293. }
  294. }
  295. - (void)scrollViewTapped:(UIGestureRecognizer *)gestureRecognizer
  296. {
  297. if (_cellState == kCellStateCenter)
  298. {
  299. if (self.isSelected)
  300. {
  301. [self deselectCell];
  302. }
  303. else if (self.shouldHighlight) // UITableView refuses selection if highlight is also refused.
  304. {
  305. [self selectCell];
  306. }
  307. }
  308. else
  309. {
  310. // Scroll back to center
  311. [self hideUtilityButtonsAnimated:YES];
  312. }
  313. }
  314. - (void)selectCell
  315. {
  316. if (_cellState == kCellStateCenter)
  317. {
  318. NSIndexPath *cellIndexPath = [self.containingTableView indexPathForCell:self];
  319. if ([self.containingTableView.delegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)])
  320. {
  321. cellIndexPath = [self.containingTableView.delegate tableView:self.containingTableView willSelectRowAtIndexPath:cellIndexPath];
  322. }
  323. if (cellIndexPath)
  324. {
  325. [self.containingTableView selectRowAtIndexPath:cellIndexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
  326. if ([self.containingTableView.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)])
  327. {
  328. [self.containingTableView.delegate tableView:self.containingTableView didSelectRowAtIndexPath:cellIndexPath];
  329. }
  330. }
  331. }
  332. }
  333. - (void)deselectCell
  334. {
  335. if (_cellState == kCellStateCenter)
  336. {
  337. NSIndexPath *cellIndexPath = [self.containingTableView indexPathForCell:self];
  338. if ([self.containingTableView.delegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)])
  339. {
  340. cellIndexPath = [self.containingTableView.delegate tableView:self.containingTableView willDeselectRowAtIndexPath:cellIndexPath];
  341. }
  342. if (cellIndexPath)
  343. {
  344. [self.containingTableView deselectRowAtIndexPath:cellIndexPath animated:NO];
  345. if ([self.containingTableView.delegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)])
  346. {
  347. [self.containingTableView.delegate tableView:self.containingTableView didDeselectRowAtIndexPath:cellIndexPath];
  348. }
  349. }
  350. }
  351. }
  352. #pragma mark - Utility buttons handling
  353. - (void)rightUtilityButtonHandler:(id)sender
  354. {
  355. SWUtilityButtonTapGestureRecognizer *utilityButtonTapGestureRecognizer = (SWUtilityButtonTapGestureRecognizer *)sender;
  356. NSUInteger utilityButtonIndex = utilityButtonTapGestureRecognizer.buttonIndex;
  357. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:didTriggerRightUtilityButtonWithIndex:)])
  358. {
  359. [self.delegate swipeableTableViewCell:self didTriggerRightUtilityButtonWithIndex:utilityButtonIndex];
  360. }
  361. }
  362. - (void)leftUtilityButtonHandler:(id)sender
  363. {
  364. SWUtilityButtonTapGestureRecognizer *utilityButtonTapGestureRecognizer = (SWUtilityButtonTapGestureRecognizer *)sender;
  365. NSUInteger utilityButtonIndex = utilityButtonTapGestureRecognizer.buttonIndex;
  366. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:didTriggerLeftUtilityButtonWithIndex:)])
  367. {
  368. [self.delegate swipeableTableViewCell:self didTriggerLeftUtilityButtonWithIndex:utilityButtonIndex];
  369. }
  370. }
  371. - (void)hideUtilityButtonsAnimated:(BOOL)animated
  372. {
  373. if (_cellState != kCellStateCenter)
  374. {
  375. [self.cellScrollView setContentOffset:[self contentOffsetForCellState:kCellStateCenter] animated:animated];
  376. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:scrollingToState:)])
  377. {
  378. [self.delegate swipeableTableViewCell:self scrollingToState:kCellStateCenter];
  379. }
  380. }
  381. }
  382. - (void)showLeftUtilityButtonsAnimated:(BOOL)animated {
  383. if (_cellState != kCellStateLeft)
  384. {
  385. [self.cellScrollView setContentOffset:[self contentOffsetForCellState:kCellStateLeft] animated:animated];
  386. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:scrollingToState:)])
  387. {
  388. [self.delegate swipeableTableViewCell:self scrollingToState:kCellStateLeft];
  389. }
  390. }
  391. }
  392. - (void)showRightUtilityButtonsAnimated:(BOOL)animated {
  393. if (_cellState != kCellStateRight)
  394. {
  395. [self.cellScrollView setContentOffset:[self contentOffsetForCellState:kCellStateRight] animated:animated];
  396. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:scrollingToState:)])
  397. {
  398. [self.delegate swipeableTableViewCell:self scrollingToState:kCellStateRight];
  399. }
  400. }
  401. }
  402. - (BOOL)isUtilityButtonsHidden {
  403. return _cellState == kCellStateCenter;
  404. }
  405. #pragma mark - Geometry helpers
  406. - (CGFloat)leftUtilityButtonsWidth
  407. {
  408. return CGRectGetWidth(self.leftUtilityButtonsView.frame);
  409. }
  410. - (CGFloat)rightUtilityButtonsWidth
  411. {
  412. return CGRectGetWidth(self.rightUtilityButtonsView.frame) + self.additionalRightPadding;
  413. }
  414. - (CGFloat)utilityButtonsPadding
  415. {
  416. return [self leftUtilityButtonsWidth] + [self rightUtilityButtonsWidth];
  417. }
  418. - (CGPoint)contentOffsetForCellState:(SWCellState)state
  419. {
  420. CGPoint scrollPt = CGPointZero;
  421. switch (state)
  422. {
  423. case kCellStateCenter:
  424. scrollPt.x = [self leftUtilityButtonsWidth];
  425. break;
  426. case kCellStateRight:
  427. scrollPt.x = [self utilityButtonsPadding];
  428. break;
  429. case kCellStateLeft:
  430. scrollPt.x = 0;
  431. break;
  432. }
  433. return scrollPt;
  434. }
  435. - (void)updateCellState
  436. {
  437. // Update the cell state according to the current scroll view contentOffset.
  438. for (NSNumber *numState in @[
  439. @(kCellStateCenter),
  440. @(kCellStateLeft),
  441. @(kCellStateRight),
  442. ])
  443. {
  444. SWCellState cellState = numState.integerValue;
  445. if (CGPointEqualToPoint(self.cellScrollView.contentOffset, [self contentOffsetForCellState:cellState]))
  446. {
  447. _cellState = cellState;
  448. break;
  449. }
  450. }
  451. // Update the clipping on the utility button views according to the current position.
  452. CGRect frame = [self.contentView.superview convertRect:self.contentView.frame toView:self];
  453. frame.size.width = CGRectGetWidth(self.frame);
  454. self.leftUtilityClipConstraint.constant = MAX(0, CGRectGetMinX(frame) - CGRectGetMinX(self.frame));
  455. self.rightUtilityClipConstraint.constant = MIN(0, CGRectGetMaxX(frame) - CGRectGetMaxX(self.frame));
  456. if (self.isEditing) {
  457. self.leftUtilityClipConstraint.constant = 0;
  458. self.cellScrollView.contentOffset = CGPointMake([self leftUtilityButtonsWidth], 0);
  459. _cellState = kCellStateCenter;
  460. }
  461. self.leftUtilityClipView.hidden = (self.leftUtilityClipConstraint.constant == 0);
  462. self.rightUtilityClipView.hidden = (self.rightUtilityClipConstraint.constant == 0);
  463. if (self.accessoryType != UITableViewCellAccessoryNone && !self.editing) {
  464. UIView *accessory = [self.cellScrollView.superview.subviews lastObject];
  465. CGRect accessoryFrame = accessory.frame;
  466. accessoryFrame.origin.x = CGRectGetWidth(frame) - CGRectGetWidth(accessoryFrame) - kAccessoryTrailingSpace + CGRectGetMinX(frame);
  467. accessory.frame = accessoryFrame;
  468. }
  469. // Enable or disable the gesture recognizers according to the current mode.
  470. if (!self.cellScrollView.isDragging && !self.cellScrollView.isDecelerating)
  471. {
  472. self.tapGestureRecognizer.enabled = YES;
  473. self.longPressGestureRecognizer.enabled = (_cellState == kCellStateCenter);
  474. }
  475. else
  476. {
  477. self.tapGestureRecognizer.enabled = NO;
  478. self.longPressGestureRecognizer.enabled = NO;
  479. }
  480. self.cellScrollView.scrollEnabled = !self.isEditing;
  481. }
  482. #pragma mark - UIScrollViewDelegate
  483. - (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
  484. {
  485. if (velocity.x >= 0.5f)
  486. {
  487. if (_cellState == kCellStateLeft || !self.rightUtilityButtons || self.rightUtilityButtonsWidth == 0.0)
  488. {
  489. _cellState = kCellStateCenter;
  490. }
  491. else
  492. {
  493. _cellState = kCellStateRight;
  494. }
  495. }
  496. else if (velocity.x <= -0.5f)
  497. {
  498. if (_cellState == kCellStateRight || !self.leftUtilityButtons || self.leftUtilityButtonsWidth == 0.0)
  499. {
  500. _cellState = kCellStateCenter;
  501. }
  502. else
  503. {
  504. _cellState = kCellStateLeft;
  505. }
  506. }
  507. else
  508. {
  509. CGFloat leftThreshold = [self contentOffsetForCellState:kCellStateLeft].x + (self.leftUtilityButtonsWidth / 2);
  510. CGFloat rightThreshold = [self contentOffsetForCellState:kCellStateRight].x - (self.rightUtilityButtonsWidth / 2);
  511. if (targetContentOffset->x > rightThreshold)
  512. {
  513. _cellState = kCellStateRight;
  514. }
  515. else if (targetContentOffset->x < leftThreshold)
  516. {
  517. _cellState = kCellStateLeft;
  518. }
  519. else
  520. {
  521. _cellState = kCellStateCenter;
  522. }
  523. }
  524. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCell:scrollingToState:)])
  525. {
  526. [self.delegate swipeableTableViewCell:self scrollingToState:_cellState];
  527. }
  528. if (_cellState != kCellStateCenter)
  529. {
  530. if ([self.delegate respondsToSelector:@selector(swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:)])
  531. {
  532. for (SWTableViewCell *cell in [self.containingTableView visibleCells]) {
  533. if (cell != self && [cell isKindOfClass:[SWTableViewCell class]] && [self.delegate swipeableTableViewCellShouldHideUtilityButtonsOnSwipe:cell]) {
  534. [cell hideUtilityButtonsAnimated:YES];
  535. }
  536. }
  537. }
  538. }
  539. *targetContentOffset = [self contentOffsetForCellState:_cellState];
  540. }
  541. - (void)scrollViewDidScroll:(UIScrollView *)scrollView
  542. {
  543. if (scrollView.contentOffset.x > [self leftUtilityButtonsWidth])
  544. {
  545. if ([self rightUtilityButtonsWidth] > 0)
  546. {
  547. if (self.delegate && [self.delegate respondsToSelector:@selector(swipeableTableViewCell:canSwipeToState:)])
  548. {
  549. BOOL shouldScroll = [self.delegate swipeableTableViewCell:self canSwipeToState:kCellStateRight];
  550. if (!shouldScroll)
  551. {
  552. scrollView.contentOffset = CGPointMake([self leftUtilityButtonsWidth], 0);
  553. }
  554. }
  555. }
  556. else
  557. {
  558. [scrollView setContentOffset:CGPointMake([self leftUtilityButtonsWidth], 0)];
  559. self.tapGestureRecognizer.enabled = YES;
  560. }
  561. }
  562. else
  563. {
  564. // Expose the left button view
  565. if ([self leftUtilityButtonsWidth] > 0)
  566. {
  567. if (self.delegate && [self.delegate respondsToSelector:@selector(swipeableTableViewCell:canSwipeToState:)])
  568. {
  569. BOOL shouldScroll = [self.delegate swipeableTableViewCell:self canSwipeToState:kCellStateLeft];
  570. if (!shouldScroll)
  571. {
  572. scrollView.contentOffset = CGPointMake([self leftUtilityButtonsWidth], 0);
  573. }
  574. }
  575. }
  576. else
  577. {
  578. [scrollView setContentOffset:CGPointMake(0, 0)];
  579. self.tapGestureRecognizer.enabled = YES;
  580. }
  581. }
  582. [self updateCellState];
  583. }
  584. - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
  585. {
  586. [self updateCellState];
  587. if (self.delegate && [self.delegate respondsToSelector:@selector(swipeableTableViewCellDidEndScrolling:)]) {
  588. [self.delegate swipeableTableViewCellDidEndScrolling:self];
  589. }
  590. }
  591. - (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView
  592. {
  593. [self updateCellState];
  594. if (self.delegate && [self.delegate respondsToSelector:@selector(swipeableTableViewCellDidEndScrolling:)]) {
  595. [self.delegate swipeableTableViewCellDidEndScrolling:self];
  596. }
  597. }
  598. -(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  599. {
  600. if (!decelerate)
  601. {
  602. self.tapGestureRecognizer.enabled = YES;
  603. }
  604. }
  605. #pragma mark - UIGestureRecognizerDelegate
  606. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
  607. {
  608. if ((gestureRecognizer == self.containingTableView.panGestureRecognizer && otherGestureRecognizer == self.longPressGestureRecognizer)
  609. || (gestureRecognizer == self.longPressGestureRecognizer && otherGestureRecognizer == self.containingTableView.panGestureRecognizer))
  610. {
  611. // Return YES so the pan gesture of the containing table view is not cancelled by the long press recognizer
  612. return YES;
  613. }
  614. else
  615. {
  616. return NO;
  617. }
  618. }
  619. -(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
  620. {
  621. return ![touch.view isKindOfClass:[UIControl class]];
  622. }
  623. @end