BGSwipeView.m 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. //
  2. // BGSwipeView.m
  3. // BuguLive
  4. //
  5. // Created by 朱庆彬 on 2017/8/16.
  6. // Copyright © 2017年 xfg. All rights reserved.
  7. //
  8. #import "BGSwipeView.h"
  9. #import <objc/message.h>
  10. #pragma GCC diagnostic ignored "-Wdirect-ivar-access"
  11. #pragma GCC diagnostic ignored "-Warc-repeated-use-of-weak"
  12. #pragma GCC diagnostic ignored "-Wreceiver-is-weak"
  13. #pragma GCC diagnostic ignored "-Wconversion"
  14. #pragma GCC diagnostic ignored "-Wselector"
  15. #pragma GCC diagnostic ignored "-Wgnu"
  16. #import <Availability.h>
  17. #if !__has_feature(objc_arc)
  18. #error This class requires automatic reference counting
  19. #endif
  20. @implementation NSObject (BGSwipeView)
  21. - (CGSize)swipeViewItemSize:(__unused BGSwipeView *)swipeView { return CGSizeZero; }
  22. - (void)swipeViewDidScroll:(__unused BGSwipeView *)swipeView {}
  23. - (void)swipeViewCurrentItemIndexDidChange:(__unused BGSwipeView *)swipeView {}
  24. - (void)swipeViewWillBeginDragging:(__unused BGSwipeView *)swipeView {}
  25. - (void)swipeViewDidEndDragging:(__unused BGSwipeView *)swipeView willDecelerate:(__unused BOOL)decelerate {}
  26. - (void)swipeViewWillBeginDecelerating:(__unused BGSwipeView *)swipeView {}
  27. - (void)swipeViewDidEndDecelerating:(__unused BGSwipeView *)swipeView {}
  28. - (void)swipeViewDidEndScrollingAnimation:(__unused BGSwipeView *)swipeView {}
  29. - (BOOL)swipeView:(__unused BGSwipeView *)swipeView shouldSelectItemAtIndex:(__unused NSInteger)index { return YES; }
  30. - (void)swipeView:(__unused BGSwipeView *)swipeView didSelectItemAtIndex:(__unused NSInteger)index {}
  31. @end
  32. @interface BGSwipeView () <UIScrollViewDelegate, UIGestureRecognizerDelegate>
  33. @property (nonatomic, strong) UIScrollView *scrollView;
  34. @property (nonatomic, strong) NSMutableDictionary *itemViews;
  35. @property (nonatomic, strong) NSMutableSet *itemViewPool;
  36. @property (nonatomic, assign) NSInteger previousItemIndex;
  37. @property (nonatomic, assign) CGPoint previousContentOffset;
  38. @property (nonatomic, assign) CGSize itemSize;
  39. @property (nonatomic, assign) BOOL suppressScrollEvent;
  40. @property (nonatomic, assign) NSTimeInterval scrollDuration;
  41. @property (nonatomic, assign, getter=isScrolling) BOOL scrolling;
  42. @property (nonatomic, assign) NSTimeInterval startTime;
  43. @property (nonatomic, assign) NSTimeInterval lastTime;
  44. @property (nonatomic, assign) CGFloat startOffset;
  45. @property (nonatomic, assign) CGFloat endOffset;
  46. @property (nonatomic, assign) CGFloat lastUpdateOffset;
  47. @property (nonatomic, strong) NSTimer *timer;
  48. @end
  49. @implementation BGSwipeView
  50. #pragma mark -
  51. #pragma mark Initialisation
  52. - (void)setUp
  53. {
  54. _scrollEnabled = YES;
  55. _pagingEnabled = YES;
  56. _delaysContentTouches = YES;
  57. _bounces = YES;
  58. _wrapEnabled = NO;
  59. _itemsPerPage = 1;
  60. _truncateFinalPage = NO;
  61. _defersItemViewLoading = NO;
  62. _vertical = NO;
  63. _scrollView = [[UIScrollView alloc] init];
  64. _scrollView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
  65. _scrollView.autoresizesSubviews = YES;
  66. _scrollView.delegate = self;
  67. _scrollView.delaysContentTouches = _delaysContentTouches;
  68. _scrollView.bounces = _bounces && !_wrapEnabled;
  69. _scrollView.alwaysBounceHorizontal = !_vertical && _bounces;
  70. _scrollView.alwaysBounceVertical = _vertical && _bounces;
  71. _scrollView.pagingEnabled = _pagingEnabled;
  72. _scrollView.scrollEnabled = _scrollEnabled;
  73. _scrollView.decelerationRate = _decelerationRate;
  74. _scrollView.showsHorizontalScrollIndicator = NO;
  75. _scrollView.showsVerticalScrollIndicator = NO;
  76. _scrollView.scrollsToTop = NO;
  77. _scrollView.clipsToBounds = NO;
  78. _decelerationRate = _scrollView.decelerationRate;
  79. _itemViews = [[NSMutableDictionary alloc] init];
  80. _previousItemIndex = 0;
  81. _previousContentOffset = _scrollView.contentOffset;
  82. _scrollOffset = 0.0f;
  83. _currentItemIndex = 0;
  84. _numberOfItems = 0;
  85. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTap:)];
  86. tapGesture.delegate = self;
  87. [_scrollView addGestureRecognizer:tapGesture];
  88. self.clipsToBounds = YES;
  89. //place scrollview at bottom of hierarchy
  90. [self insertSubview:_scrollView atIndex:0];
  91. if (_dataSource)
  92. {
  93. [self reloadData];
  94. }
  95. }
  96. - (id)initWithCoder:(NSCoder *)aDecoder
  97. {
  98. if ((self = [super initWithCoder:aDecoder]))
  99. {
  100. [self setUp];
  101. }
  102. return self;
  103. }
  104. - (id)initWithFrame:(CGRect)frame
  105. {
  106. if ((self = [super initWithFrame:frame]))
  107. {
  108. [self setUp];
  109. }
  110. return self;
  111. }
  112. - (void)dealloc
  113. {
  114. [_timer invalidate];
  115. }
  116. - (void)setDataSource:(id<FWSwipeViewDataSource>)dataSource
  117. {
  118. if (_dataSource != dataSource)
  119. {
  120. _dataSource = dataSource;
  121. if (_dataSource)
  122. {
  123. [self reloadData];
  124. }
  125. }
  126. }
  127. - (void)setDelegate:(id<FWSwipeViewDelegate>)delegate
  128. {
  129. if (_delegate != delegate)
  130. {
  131. _delegate = delegate;
  132. [self setNeedsLayout];
  133. }
  134. }
  135. - (void)setAlignment:(FWSwipeViewAlignment)alignment
  136. {
  137. if (_alignment != alignment)
  138. {
  139. _alignment = alignment;
  140. [self setNeedsLayout];
  141. }
  142. }
  143. - (void)setItemsPerPage:(NSInteger)itemsPerPage
  144. {
  145. if (_itemsPerPage != itemsPerPage)
  146. {
  147. _itemsPerPage = itemsPerPage;
  148. [self setNeedsLayout];
  149. }
  150. }
  151. - (void)setTruncateFinalPage:(BOOL)truncateFinalPage
  152. {
  153. if (_truncateFinalPage != truncateFinalPage)
  154. {
  155. _truncateFinalPage = truncateFinalPage;
  156. [self setNeedsLayout];
  157. }
  158. }
  159. - (void)setScrollEnabled:(BOOL)scrollEnabled
  160. {
  161. if (_scrollEnabled != scrollEnabled)
  162. {
  163. _scrollEnabled = scrollEnabled;
  164. _scrollView.scrollEnabled = _scrollEnabled;
  165. }
  166. }
  167. - (void)setPagingEnabled:(BOOL)pagingEnabled
  168. {
  169. if (_pagingEnabled != pagingEnabled)
  170. {
  171. _pagingEnabled = pagingEnabled;
  172. _scrollView.pagingEnabled = pagingEnabled;
  173. [self setNeedsLayout];
  174. }
  175. }
  176. - (void)setWrapEnabled:(BOOL)wrapEnabled
  177. {
  178. if (_wrapEnabled != wrapEnabled)
  179. {
  180. CGFloat previousOffset = [self clampedOffset:_scrollOffset];
  181. _wrapEnabled = wrapEnabled;
  182. _scrollView.bounces = _bounces && !_wrapEnabled;
  183. [self setNeedsLayout];
  184. [self layoutIfNeeded];
  185. self.scrollOffset = previousOffset;
  186. }
  187. }
  188. - (void)setDelaysContentTouches:(BOOL)delaysContentTouches
  189. {
  190. _delaysContentTouches = delaysContentTouches;
  191. _scrollView.delaysContentTouches = delaysContentTouches;
  192. }
  193. - (void)setBounces:(BOOL)bounces
  194. {
  195. if (_bounces != bounces)
  196. {
  197. _bounces = bounces;
  198. _scrollView.alwaysBounceHorizontal = !_vertical && _bounces;
  199. _scrollView.alwaysBounceVertical = _vertical && _bounces;
  200. _scrollView.bounces = _bounces && !_wrapEnabled;
  201. }
  202. }
  203. - (void)setDecelerationRate:(float)decelerationRate
  204. {
  205. if (fabsf(_decelerationRate - decelerationRate) > 0.0001f)
  206. {
  207. _decelerationRate = decelerationRate;
  208. _scrollView.decelerationRate = _decelerationRate;
  209. }
  210. }
  211. - (void)setAutoscroll:(CGFloat)autoscroll
  212. {
  213. if (fabs(_autoscroll - autoscroll) > 0.0001f)
  214. {
  215. _autoscroll = autoscroll;
  216. if (autoscroll)
  217. [self startAnimation];
  218. }
  219. }
  220. - (void)setVertical:(BOOL)vertical
  221. {
  222. if (_vertical != vertical)
  223. {
  224. _vertical = vertical;
  225. _scrollView.alwaysBounceHorizontal = !_vertical && _bounces;
  226. _scrollView.alwaysBounceVertical = _vertical && _bounces;
  227. [self setNeedsLayout];
  228. }
  229. }
  230. - (BOOL)isDragging
  231. {
  232. return _scrollView.dragging;
  233. }
  234. - (BOOL)isDecelerating
  235. {
  236. return _scrollView.decelerating;
  237. }
  238. #pragma mark -
  239. #pragma mark View management
  240. - (NSArray *)indexesForVisibleItems
  241. {
  242. return [[_itemViews allKeys] sortedArrayUsingSelector:@selector(compare:)];
  243. }
  244. - (NSArray *)visibleItemViews
  245. {
  246. NSArray *indexes = [self indexesForVisibleItems];
  247. return [_itemViews objectsForKeys:indexes notFoundMarker:[NSNull null]];
  248. }
  249. - (UIView *)itemViewAtIndex:(NSInteger)index
  250. {
  251. return _itemViews[@(index)];
  252. }
  253. - (UIView *)currentItemView
  254. {
  255. return [self itemViewAtIndex:_currentItemIndex];
  256. }
  257. - (NSInteger)indexOfItemView:(UIView *)view
  258. {
  259. NSUInteger index = [[_itemViews allValues] indexOfObject:view];
  260. if (index != NSNotFound)
  261. {
  262. return [[_itemViews allKeys][index] integerValue];
  263. }
  264. return NSNotFound;
  265. }
  266. - (NSInteger)indexOfItemViewOrSubview:(UIView *)view
  267. {
  268. NSInteger index = [self indexOfItemView:view];
  269. if (index == NSNotFound && view != nil && view != _scrollView)
  270. {
  271. return [self indexOfItemViewOrSubview:view.superview];
  272. }
  273. return index;
  274. }
  275. - (void)setItemView:(UIView *)view forIndex:(NSInteger)index
  276. {
  277. ((NSMutableDictionary *) _itemViews)[@(index)] = view;
  278. }
  279. #pragma mark -
  280. #pragma mark View layout
  281. - (void)updateScrollOffset
  282. {
  283. if (_wrapEnabled)
  284. {
  285. CGFloat itemsWide = (_numberOfItems == 1) ? 1.0f : 3.0f;
  286. if (_vertical)
  287. {
  288. CGFloat scrollHeight = _scrollView.contentSize.height / itemsWide;
  289. if (_scrollView.contentOffset.y < scrollHeight)
  290. {
  291. _previousContentOffset.y += scrollHeight;
  292. [self setContentOffsetWithoutEvent:CGPointMake(0.0f, _scrollView.contentOffset.y + scrollHeight)];
  293. }
  294. else if (_scrollView.contentOffset.y >= scrollHeight * 2.0f)
  295. {
  296. _previousContentOffset.y -= scrollHeight;
  297. [self setContentOffsetWithoutEvent:CGPointMake(0.0f, _scrollView.contentOffset.y - scrollHeight)];
  298. }
  299. _scrollOffset = [self clampedOffset:_scrollOffset];
  300. }
  301. else
  302. {
  303. CGFloat scrollWidth = _scrollView.contentSize.width / itemsWide;
  304. if (_scrollView.contentOffset.x < scrollWidth)
  305. {
  306. _previousContentOffset.x += scrollWidth;
  307. [self setContentOffsetWithoutEvent:CGPointMake(_scrollView.contentOffset.x + scrollWidth, 0.0f)];
  308. }
  309. else if (_scrollView.contentOffset.x >= scrollWidth * 2.0f)
  310. {
  311. _previousContentOffset.x -= scrollWidth;
  312. [self setContentOffsetWithoutEvent:CGPointMake(_scrollView.contentOffset.x - scrollWidth, 0.0f)];
  313. }
  314. _scrollOffset = [self clampedOffset:_scrollOffset];
  315. }
  316. }
  317. if (_vertical && fabs(_scrollView.contentOffset.x) > 0.0001f)
  318. {
  319. [self setContentOffsetWithoutEvent:CGPointMake(0.0f, _scrollView.contentOffset.y)];
  320. }
  321. else if (!_vertical && fabs(_scrollView.contentOffset.y) > 0.0001f)
  322. {
  323. [self setContentOffsetWithoutEvent:CGPointMake(_scrollView.contentOffset.x, 0.0f)];
  324. }
  325. }
  326. - (void)updateScrollViewDimensions
  327. {
  328. CGRect frame = self.bounds;
  329. CGSize contentSize = frame.size;
  330. if (_vertical)
  331. {
  332. contentSize.width -= (_scrollView.contentInset.left + _scrollView.contentInset.right);
  333. }
  334. else
  335. {
  336. contentSize.height -= (_scrollView.contentInset.top + _scrollView.contentInset.bottom);
  337. }
  338. switch (_alignment)
  339. {
  340. case FWSwipeViewAlignmentCenter:
  341. {
  342. if (_vertical)
  343. {
  344. frame = CGRectMake(0.0f, (self.bounds.size.height - _itemSize.height * _itemsPerPage) / 2.0f,
  345. self.bounds.size.width, _itemSize.height * _itemsPerPage);
  346. contentSize.height = _itemSize.height * _numberOfItems;
  347. }
  348. else
  349. {
  350. frame = CGRectMake((self.bounds.size.width - _itemSize.width * _itemsPerPage) / 2.0f,
  351. 0.0f, _itemSize.width * _itemsPerPage, self.bounds.size.height);
  352. contentSize.width = _itemSize.width * _numberOfItems;
  353. }
  354. break;
  355. }
  356. case FWSwipeViewAlignmentEdge:
  357. {
  358. if (_vertical)
  359. {
  360. frame = CGRectMake(0.0f, 0.0f, self.bounds.size.width, _itemSize.height * _itemsPerPage);
  361. contentSize.height = _itemSize.height * _numberOfItems - (self.bounds.size.height - frame.size.height);
  362. }
  363. else
  364. {
  365. frame = CGRectMake(0.0f, 0.0f, _itemSize.width * _itemsPerPage, self.bounds.size.height);
  366. contentSize.width = _itemSize.width * _numberOfItems - (self.bounds.size.width - frame.size.width);
  367. }
  368. break;
  369. }
  370. }
  371. if (_wrapEnabled)
  372. {
  373. CGFloat itemsWide = (_numberOfItems == 1) ? 1.0f : _numberOfItems * 3.0f;
  374. if (_vertical)
  375. {
  376. contentSize.height = _itemSize.height * itemsWide;
  377. }
  378. else
  379. {
  380. contentSize.width = _itemSize.width * itemsWide;
  381. }
  382. }
  383. else if (_pagingEnabled && !_truncateFinalPage)
  384. {
  385. if (_vertical)
  386. {
  387. contentSize.height = ceilf(contentSize.height / frame.size.height) * frame.size.height;
  388. }
  389. else
  390. {
  391. contentSize.width = ceilf(contentSize.width / frame.size.width) * frame.size.width;
  392. }
  393. }
  394. if (!CGRectEqualToRect(_scrollView.frame, frame))
  395. {
  396. _scrollView.frame = frame;
  397. }
  398. if (!CGSizeEqualToSize(_scrollView.contentSize, contentSize))
  399. {
  400. _scrollView.contentSize = contentSize;
  401. }
  402. }
  403. - (CGFloat)offsetForItemAtIndex:(NSInteger)index
  404. {
  405. //calculate relative position
  406. CGFloat offset = index - _scrollOffset;
  407. if (_wrapEnabled)
  408. {
  409. if (_alignment == FWSwipeViewAlignmentCenter)
  410. {
  411. if (offset > _numberOfItems / 2)
  412. {
  413. offset -= _numberOfItems;
  414. }
  415. else if (offset < -_numberOfItems / 2)
  416. {
  417. offset += _numberOfItems;
  418. }
  419. }
  420. else
  421. {
  422. CGFloat width = _vertical ? self.bounds.size.height : self.bounds.size.width;
  423. CGFloat x = _vertical ? _scrollView.frame.origin.y : _scrollView.frame.origin.x;
  424. CGFloat itemWidth = _vertical ? _itemSize.height : _itemSize.width;
  425. if (offset * itemWidth + x > width)
  426. {
  427. offset -= _numberOfItems;
  428. }
  429. else if (offset * itemWidth + x < -itemWidth)
  430. {
  431. offset += _numberOfItems;
  432. }
  433. }
  434. }
  435. return offset;
  436. }
  437. - (void)setFrameForView:(UIView *)view atIndex:(NSInteger)index
  438. {
  439. if (self.window)
  440. {
  441. CGPoint center = view.center;
  442. if (_vertical)
  443. {
  444. center.y = ([self offsetForItemAtIndex:index] + 0.5f) * _itemSize.height + _scrollView.contentOffset.y;
  445. }
  446. else
  447. {
  448. center.x = ([self offsetForItemAtIndex:index] + 0.5f) * _itemSize.width + _scrollView.contentOffset.x;
  449. }
  450. BOOL disableAnimation = !CGPointEqualToPoint(center, view.center);
  451. BOOL animationEnabled = [UIView areAnimationsEnabled];
  452. if (disableAnimation && animationEnabled)
  453. [UIView setAnimationsEnabled:NO];
  454. if (_vertical)
  455. {
  456. view.center = CGPointMake(_scrollView.frame.size.width / 2.0f, center.y);
  457. }
  458. else
  459. {
  460. view.center = CGPointMake(center.x, _scrollView.frame.size.height / 2.0f);
  461. }
  462. view.bounds = CGRectMake(0.0f, 0.0f, _itemSize.width, _itemSize.height);
  463. if (disableAnimation && animationEnabled)
  464. [UIView setAnimationsEnabled:YES];
  465. }
  466. }
  467. - (void)layOutItemViews
  468. {
  469. for (UIView *view in self.visibleItemViews)
  470. {
  471. [self setFrameForView:view atIndex:[self indexOfItemView:view]];
  472. }
  473. }
  474. - (void)updateLayout
  475. {
  476. [self updateScrollOffset];
  477. [self loadUnloadViews];
  478. [self layOutItemViews];
  479. }
  480. - (void)layoutSubviews
  481. {
  482. [super layoutSubviews];
  483. [self updateItemSizeAndCount];
  484. [self updateScrollViewDimensions];
  485. [self updateLayout];
  486. if (_pagingEnabled && !_scrolling)
  487. {
  488. [self scrollToItemAtIndex:self.currentItemIndex duration:0.25];
  489. }
  490. }
  491. #pragma mark -
  492. #pragma mark View queing
  493. - (void)queueItemView:(UIView *)view
  494. {
  495. if (view)
  496. {
  497. [_itemViewPool addObject:view];
  498. }
  499. }
  500. - (UIView *)dequeueItemView
  501. {
  502. UIView *view = [_itemViewPool anyObject];
  503. if (view)
  504. {
  505. [_itemViewPool removeObject:view];
  506. }
  507. return view;
  508. }
  509. #pragma mark -
  510. #pragma mark Scrolling
  511. - (void)didScroll
  512. {
  513. //handle wrap
  514. [self updateScrollOffset];
  515. //update view
  516. [self layOutItemViews];
  517. [_delegate swipeViewDidScroll:self];
  518. if (!_defersItemViewLoading || fabs([self minScrollDistanceFromOffset:_lastUpdateOffset toOffset:_scrollOffset]) >= 1.0f)
  519. {
  520. //update item index
  521. _currentItemIndex = [self clampedIndex:roundf(_scrollOffset)];
  522. //load views
  523. _lastUpdateOffset = _currentItemIndex;
  524. [self loadUnloadViews];
  525. //send index update event
  526. if (_previousItemIndex != _currentItemIndex)
  527. {
  528. _previousItemIndex = _currentItemIndex;
  529. [_delegate swipeViewCurrentItemIndexDidChange:self];
  530. }
  531. }
  532. }
  533. - (CGFloat)easeInOut:(CGFloat)time
  534. {
  535. return (time < 0.5f) ? 0.5f * powf(time * 2.0f, 3.0f) : 0.5f * powf(time * 2.0f - 2.0f, 3.0f) + 1.0f;
  536. }
  537. - (void)step
  538. {
  539. NSTimeInterval currentTime = CFAbsoluteTimeGetCurrent();
  540. double delta = _lastTime - currentTime;
  541. _lastTime = currentTime;
  542. if (_scrolling)
  543. {
  544. NSTimeInterval time = fminf(1.0f, (currentTime - _startTime) / _scrollDuration);
  545. delta = [self easeInOut:time];
  546. _scrollOffset = [self clampedOffset:_startOffset + (_endOffset - _startOffset) * delta];
  547. if (_vertical)
  548. {
  549. [self setContentOffsetWithoutEvent:CGPointMake(0.0f, _scrollOffset * _itemSize.height)];
  550. }
  551. else
  552. {
  553. [self setContentOffsetWithoutEvent:CGPointMake(_scrollOffset * _itemSize.width, 0.0f)];
  554. }
  555. [self didScroll];
  556. if (time == 1.0f)
  557. {
  558. _scrolling = NO;
  559. [self didScroll];
  560. [_delegate swipeViewDidEndScrollingAnimation:self];
  561. }
  562. }
  563. else if (_autoscroll)
  564. {
  565. if (!_scrollView.dragging)
  566. self.scrollOffset = [self clampedOffset:_scrollOffset + delta * _autoscroll];
  567. }
  568. else
  569. {
  570. [self stopAnimation];
  571. }
  572. }
  573. - (void)startAnimation
  574. {
  575. if (!_timer)
  576. {
  577. self.timer = [NSTimer timerWithTimeInterval:1.0 / 60.0
  578. target:self
  579. selector:@selector(step)
  580. userInfo:nil
  581. repeats:YES];
  582. [[NSRunLoop mainRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
  583. [[NSRunLoop mainRunLoop] addTimer:_timer forMode:UITrackingRunLoopMode];
  584. }
  585. }
  586. - (void)stopAnimation
  587. {
  588. [_timer invalidate];
  589. self.timer = nil;
  590. }
  591. - (NSInteger)clampedIndex:(NSInteger)index
  592. {
  593. if (_wrapEnabled)
  594. {
  595. return _numberOfItems ? (index - floorf((CGFloat) index / (CGFloat) _numberOfItems) * _numberOfItems) : 0;
  596. }
  597. else
  598. {
  599. return MIN(MAX(0, index), MAX(0, _numberOfItems - 1));
  600. }
  601. }
  602. - (CGFloat)clampedOffset:(CGFloat)offset
  603. {
  604. CGFloat returnValue = 0;
  605. if (_wrapEnabled)
  606. {
  607. returnValue = _numberOfItems ? (offset - floorf(offset / (CGFloat) _numberOfItems) * _numberOfItems) : 0.0f;
  608. }
  609. else
  610. {
  611. returnValue = fminf(fmaxf(0.0f, offset), fmaxf(0.0f, (CGFloat) _numberOfItems - 1.0f));
  612. }
  613. return returnValue;
  614. }
  615. - (void)setContentOffsetWithoutEvent:(CGPoint)contentOffset
  616. {
  617. if (!CGPointEqualToPoint(_scrollView.contentOffset, contentOffset))
  618. {
  619. BOOL animationEnabled = [UIView areAnimationsEnabled];
  620. if (animationEnabled)
  621. [UIView setAnimationsEnabled:NO];
  622. _suppressScrollEvent = YES;
  623. _scrollView.contentOffset = contentOffset;
  624. _suppressScrollEvent = NO;
  625. if (animationEnabled)
  626. [UIView setAnimationsEnabled:YES];
  627. }
  628. }
  629. - (NSInteger)currentPage
  630. {
  631. if (_itemsPerPage > 1 && _truncateFinalPage && !_wrapEnabled &&
  632. _currentItemIndex > (_numberOfItems / _itemsPerPage - 1) * _itemsPerPage)
  633. {
  634. return self.numberOfPages - 1;
  635. }
  636. return roundf((float) _currentItemIndex / (float) _itemsPerPage);
  637. }
  638. - (NSInteger)numberOfPages
  639. {
  640. return ceilf((float) self.numberOfItems / (float) _itemsPerPage);
  641. }
  642. - (NSInteger)minScrollDistanceFromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex
  643. {
  644. NSInteger directDistance = toIndex - fromIndex;
  645. if (_wrapEnabled)
  646. {
  647. NSInteger wrappedDistance = MIN(toIndex, fromIndex) + _numberOfItems - MAX(toIndex, fromIndex);
  648. if (fromIndex < toIndex)
  649. {
  650. wrappedDistance = -wrappedDistance;
  651. }
  652. return (ABS(directDistance) <= ABS(wrappedDistance)) ? directDistance : wrappedDistance;
  653. }
  654. return directDistance;
  655. }
  656. - (CGFloat)minScrollDistanceFromOffset:(CGFloat)fromOffset toOffset:(CGFloat)toOffset
  657. {
  658. CGFloat directDistance = toOffset - fromOffset;
  659. if (_wrapEnabled)
  660. {
  661. CGFloat wrappedDistance = fminf(toOffset, fromOffset) + _numberOfItems - fmaxf(toOffset, fromOffset);
  662. if (fromOffset < toOffset)
  663. {
  664. wrappedDistance = -wrappedDistance;
  665. }
  666. return (fabs(directDistance) <= fabs(wrappedDistance)) ? directDistance : wrappedDistance;
  667. }
  668. return directDistance;
  669. }
  670. - (void)setCurrentItemIndex:(NSInteger)currentItemIndex
  671. {
  672. _currentItemIndex = currentItemIndex;
  673. self.scrollOffset = currentItemIndex;
  674. }
  675. - (void)setCurrentPage:(NSInteger)currentPage
  676. {
  677. if (currentPage * _itemsPerPage != _currentItemIndex)
  678. {
  679. [self scrollToPage:currentPage duration:0.0];
  680. }
  681. }
  682. - (void)setScrollOffset:(CGFloat)scrollOffset
  683. {
  684. if (fabs(_scrollOffset - scrollOffset) > 0.0001f)
  685. {
  686. _scrollOffset = scrollOffset;
  687. _lastUpdateOffset = _scrollOffset - 1.0f; //force refresh
  688. _scrolling = NO; //stop scrolling
  689. [self updateItemSizeAndCount];
  690. [self updateScrollViewDimensions];
  691. [self updateLayout];
  692. CGPoint contentOffset = _vertical ? CGPointMake(0.0f, [self clampedOffset:scrollOffset] * _itemSize.height) : CGPointMake([self clampedOffset:scrollOffset] * _itemSize.width, 0.0f);
  693. [self setContentOffsetWithoutEvent:contentOffset];
  694. [self didScroll];
  695. }
  696. }
  697. - (void)scrollByOffset:(CGFloat)offset duration:(NSTimeInterval)duration
  698. {
  699. if (duration > 0.0)
  700. {
  701. _scrolling = YES;
  702. _startTime = [[NSDate date] timeIntervalSinceReferenceDate];
  703. _startOffset = _scrollOffset;
  704. _scrollDuration = duration;
  705. _endOffset = _startOffset + offset;
  706. if (!_wrapEnabled)
  707. {
  708. _endOffset = [self clampedOffset:_endOffset];
  709. }
  710. [self startAnimation];
  711. }
  712. else
  713. {
  714. self.scrollOffset += offset;
  715. }
  716. }
  717. - (void)scrollToOffset:(CGFloat)offset duration:(NSTimeInterval)duration
  718. {
  719. [self scrollByOffset:[self minScrollDistanceFromOffset:_scrollOffset toOffset:offset] duration:duration];
  720. }
  721. - (void)scrollByNumberOfItems:(NSInteger)itemCount duration:(NSTimeInterval)duration
  722. {
  723. if (duration > 0.0)
  724. {
  725. CGFloat offset = 0.0f;
  726. if (itemCount > 0)
  727. {
  728. offset = (floorf(_scrollOffset) + itemCount) - _scrollOffset;
  729. }
  730. else if (itemCount < 0)
  731. {
  732. offset = (ceilf(_scrollOffset) + itemCount) - _scrollOffset;
  733. }
  734. else
  735. {
  736. offset = roundf(_scrollOffset) - _scrollOffset;
  737. }
  738. [self scrollByOffset:offset duration:duration];
  739. }
  740. else
  741. {
  742. self.scrollOffset = [self clampedIndex:_previousItemIndex + itemCount];
  743. }
  744. }
  745. - (void)scrollToItemAtIndex:(NSInteger)index duration:(NSTimeInterval)duration
  746. {
  747. [self scrollToOffset:index duration:duration];
  748. }
  749. - (void)scrollToPage:(NSInteger)page duration:(NSTimeInterval)duration
  750. {
  751. NSInteger index = page * _itemsPerPage;
  752. if (_truncateFinalPage)
  753. {
  754. index = MIN(index, _numberOfItems - _itemsPerPage);
  755. }
  756. [self scrollToItemAtIndex:index duration:duration];
  757. }
  758. #pragma mark -
  759. #pragma mark View loading
  760. - (UIView *)loadViewAtIndex:(NSInteger)index
  761. {
  762. UIView *view = [_dataSource swipeView:self viewForItemAtIndex:index reusingView:[self dequeueItemView]];
  763. if (view == nil)
  764. {
  765. view = [[UIView alloc] init];
  766. }
  767. UIView *oldView = [self itemViewAtIndex:index];
  768. if (oldView)
  769. {
  770. [self queueItemView:oldView];
  771. [oldView removeFromSuperview];
  772. }
  773. [self setItemView:view forIndex:index];
  774. [self setFrameForView:view atIndex:index];
  775. view.userInteractionEnabled = YES;
  776. [_scrollView addSubview:view];
  777. return view;
  778. }
  779. - (void)updateItemSizeAndCount
  780. {
  781. //get number of items
  782. _numberOfItems = [_dataSource numberOfItemsInFWSwipeView:self];
  783. //get item size
  784. CGSize size = [_delegate swipeViewItemSize:self];
  785. if (!CGSizeEqualToSize(size, CGSizeZero))
  786. {
  787. _itemSize = size;
  788. }
  789. else if (_numberOfItems > 0)
  790. {
  791. UIView *view = [[self visibleItemViews] lastObject] ?: [_dataSource swipeView:self viewForItemAtIndex:0 reusingView:[self dequeueItemView]];
  792. _itemSize = view.frame.size;
  793. }
  794. //prevent crashes
  795. if (_itemSize.width < 0.0001)
  796. _itemSize.width = 1;
  797. if (_itemSize.height < 0.0001)
  798. _itemSize.height = 1;
  799. }
  800. - (void)loadUnloadViews
  801. {
  802. //check that item size is known
  803. CGFloat itemWidth = _vertical ? _itemSize.height : _itemSize.width;
  804. if (itemWidth)
  805. {
  806. //calculate offset and bounds
  807. CGFloat width = _vertical ? self.bounds.size.height : self.bounds.size.width;
  808. CGFloat x = _vertical ? _scrollView.frame.origin.y : _scrollView.frame.origin.x;
  809. //calculate range
  810. CGFloat startOffset = [self clampedOffset:_scrollOffset - x / itemWidth];
  811. NSInteger startIndex = floorf(startOffset);
  812. NSInteger numberOfVisibleItems = ceilf(width / itemWidth + (startOffset - startIndex));
  813. if (_defersItemViewLoading)
  814. {
  815. startIndex = _currentItemIndex - ceilf(x / itemWidth) - 1;
  816. numberOfVisibleItems = ceilf(width / itemWidth) + 3;
  817. }
  818. //create indices
  819. numberOfVisibleItems = MIN(numberOfVisibleItems, _numberOfItems);
  820. NSMutableSet *visibleIndices = [NSMutableSet setWithCapacity:numberOfVisibleItems];
  821. for (NSInteger i = 0; i < numberOfVisibleItems; i++)
  822. {
  823. NSInteger index = [self clampedIndex:i + startIndex];
  824. [visibleIndices addObject:@(index)];
  825. }
  826. //remove offscreen views
  827. for (NSNumber *number in [_itemViews allKeys])
  828. {
  829. if (![visibleIndices containsObject:number])
  830. {
  831. UIView *view = _itemViews[number];
  832. [self queueItemView:view];
  833. [view removeFromSuperview];
  834. [_itemViews removeObjectForKey:number];
  835. }
  836. }
  837. //add onscreen views
  838. for (NSNumber *number in visibleIndices)
  839. {
  840. UIView *view = _itemViews[number];
  841. if (view == nil)
  842. {
  843. [self loadViewAtIndex:[number integerValue]];
  844. }
  845. }
  846. }
  847. }
  848. - (void)reloadItemAtIndex:(NSInteger)index
  849. {
  850. //if view is visible
  851. if ([self itemViewAtIndex:index])
  852. {
  853. //reload view
  854. [self loadViewAtIndex:index];
  855. }
  856. }
  857. - (void)reloadData
  858. {
  859. //remove old views
  860. for (UIView *view in self.visibleItemViews)
  861. {
  862. [view removeFromSuperview];
  863. }
  864. //reset view pools
  865. self.itemViews = [NSMutableDictionary dictionary];
  866. self.itemViewPool = [NSMutableSet set];
  867. //get number of items
  868. [self updateItemSizeAndCount];
  869. //layout views
  870. [self setNeedsLayout];
  871. //fix scroll offset
  872. if (_numberOfItems > 0 && _scrollOffset < 0.0f)
  873. {
  874. self.scrollOffset = 0;
  875. }
  876. }
  877. - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
  878. {
  879. UIView *view = [super hitTest:point withEvent:event];
  880. if ([view isEqual:self])
  881. {
  882. for (UIView *subview in _scrollView.subviews)
  883. {
  884. CGPoint offset = CGPointMake(point.x - _scrollView.frame.origin.x + _scrollView.contentOffset.x - subview.frame.origin.x,
  885. point.y - _scrollView.frame.origin.y + _scrollView.contentOffset.y - subview.frame.origin.y);
  886. if ((view = [subview hitTest:offset withEvent:event]))
  887. {
  888. return view;
  889. }
  890. }
  891. return _scrollView;
  892. }
  893. return view;
  894. }
  895. - (void)didMoveToSuperview
  896. {
  897. if (self.superview)
  898. {
  899. [self setNeedsLayout];
  900. if (_scrolling)
  901. {
  902. [self startAnimation];
  903. }
  904. }
  905. else
  906. {
  907. [self stopAnimation];
  908. }
  909. }
  910. #pragma mark -
  911. #pragma mark Gestures and taps
  912. - (NSInteger)viewOrSuperviewIndex:(UIView *)view
  913. {
  914. if (view == nil || view == _scrollView)
  915. {
  916. return NSNotFound;
  917. }
  918. NSInteger index = [self indexOfItemView:view];
  919. if (index == NSNotFound)
  920. {
  921. return [self viewOrSuperviewIndex:view.superview];
  922. }
  923. return index;
  924. }
  925. - (BOOL)viewOrSuperviewHandlesTouches:(UIView *)view
  926. {
  927. //thanks to @mattjgalloway and @shaps for idea
  928. //https://gist.github.com/mattjgalloway/6279363
  929. //https://gist.github.com/shaps80/6279008
  930. Class class = [view class];
  931. while (class && class != [UIView class])
  932. {
  933. unsigned int numberOfMethods;
  934. Method *methods = class_copyMethodList(class, &numberOfMethods);
  935. for (unsigned int i = 0; i < numberOfMethods; i++)
  936. {
  937. if (method_getName(methods[i]) == @selector(touchesBegan:withEvent:))
  938. {
  939. free(methods);
  940. return YES;
  941. }
  942. }
  943. if (methods)
  944. free(methods);
  945. class = [class superclass];
  946. }
  947. if (view.superview && view.superview != _scrollView)
  948. {
  949. return [self viewOrSuperviewHandlesTouches:view.superview];
  950. }
  951. return NO;
  952. }
  953. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gesture shouldReceiveTouch:(UITouch *)touch
  954. {
  955. if ([gesture isKindOfClass:[UITapGestureRecognizer class]])
  956. {
  957. //handle tap
  958. NSInteger index = [self viewOrSuperviewIndex:touch.view];
  959. if (index != NSNotFound)
  960. {
  961. if ((_delegate && ![_delegate swipeView:self shouldSelectItemAtIndex:index]) ||
  962. [self viewOrSuperviewHandlesTouches:touch.view])
  963. {
  964. return NO;
  965. }
  966. else
  967. {
  968. return YES;
  969. }
  970. }
  971. }
  972. return NO;
  973. }
  974. - (void)didTap:(UITapGestureRecognizer *)tapGesture
  975. {
  976. CGPoint point = [tapGesture locationInView:_scrollView];
  977. NSInteger index = _vertical ? (point.y / (_itemSize.height)) : (point.x / (_itemSize.width));
  978. if (_wrapEnabled)
  979. {
  980. index = index % _numberOfItems;
  981. }
  982. if (index >= 0 && index < _numberOfItems)
  983. {
  984. [_delegate swipeView:self didSelectItemAtIndex:index];
  985. }
  986. }
  987. #pragma mark -
  988. #pragma mark UIScrollViewDelegate methods
  989. - (void)scrollViewDidScroll:(__unused UIScrollView *)scrollView
  990. {
  991. if (!_suppressScrollEvent)
  992. {
  993. //stop scrolling animation
  994. _scrolling = NO;
  995. //update scrollOffset
  996. CGFloat delta = _vertical ? (_scrollView.contentOffset.y - _previousContentOffset.y) : (_scrollView.contentOffset.x - _previousContentOffset.x);
  997. _previousContentOffset = _scrollView.contentOffset;
  998. _scrollOffset += delta / (_vertical ? _itemSize.height : _itemSize.width);
  999. //update view and call delegate
  1000. [self didScroll];
  1001. }
  1002. else
  1003. {
  1004. _previousContentOffset = _scrollView.contentOffset;
  1005. }
  1006. }
  1007. - (void)scrollViewWillBeginDragging:(__unused UIScrollView *)scrollView
  1008. {
  1009. [_delegate swipeViewWillBeginDragging:self];
  1010. //force refresh
  1011. _lastUpdateOffset = self.scrollOffset - 1.0f;
  1012. [self didScroll];
  1013. }
  1014. - (void)scrollViewDidEndDragging:(__unused UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
  1015. {
  1016. if (!decelerate)
  1017. {
  1018. //force refresh
  1019. _lastUpdateOffset = self.scrollOffset - 1.0f;
  1020. [self didScroll];
  1021. }
  1022. [_delegate swipeViewDidEndDragging:self willDecelerate:decelerate];
  1023. }
  1024. - (void)scrollViewWillBeginDecelerating:(__unused UIScrollView *)scrollView
  1025. {
  1026. [_delegate swipeViewWillBeginDecelerating:self];
  1027. }
  1028. - (void)scrollViewDidEndDecelerating:(__unused UIScrollView *)scrollView
  1029. {
  1030. //prevent rounding errors from accumulating
  1031. CGFloat integerOffset = roundf(_scrollOffset);
  1032. if (fabs(_scrollOffset - integerOffset) < 0.01f)
  1033. {
  1034. _scrollOffset = integerOffset;
  1035. }
  1036. //force refresh
  1037. _lastUpdateOffset = self.scrollOffset - 1.0f;
  1038. [self didScroll];
  1039. [_delegate swipeViewDidEndDecelerating:self];
  1040. }
  1041. @end