FSPagerView.swift 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636
  1. //
  2. // FSPagerView.swift
  3. // FSPagerView
  4. //
  5. // Created by Wenchao Ding on 17/12/2016.
  6. // Copyright © 2016 Wenchao Ding. All rights reserved.
  7. //
  8. // https://github.com/WenchaoD
  9. //
  10. // FSPagerView is an elegant Screen Slide Library implemented primarily with UICollectionView. It is extremely helpful for making Banner、Product Show、Welcome/Guide Pages、Screen/ViewController Sliders.
  11. //
  12. import UIKit
  13. @objc
  14. public protocol FSPagerViewDataSource: NSObjectProtocol {
  15. /// Asks your data source object for the number of items in the pager view.
  16. @objc(numberOfItemsInPagerView:)
  17. func numberOfItems(in pagerView: FSPagerView) -> Int
  18. /// Asks your data source object for the cell that corresponds to the specified item in the pager view.
  19. @objc(pagerView:cellForItemAtIndex:)
  20. func pagerView(_ pagerView: FSPagerView, cellForItemAt index: Int) -> FSPagerViewCell
  21. }
  22. @objc
  23. public protocol FSPagerViewDelegate: NSObjectProtocol {
  24. /// Asks the delegate if the item should be highlighted during tracking.
  25. @objc(pagerView:shouldHighlightItemAtIndex:)
  26. optional func pagerView(_ pagerView: FSPagerView, shouldHighlightItemAt index: Int) -> Bool
  27. /// Tells the delegate that the item at the specified index was highlighted.
  28. @objc(pagerView:didHighlightItemAtIndex:)
  29. optional func pagerView(_ pagerView: FSPagerView, didHighlightItemAt index: Int)
  30. /// Asks the delegate if the specified item should be selected.
  31. @objc(pagerView:shouldSelectItemAtIndex:)
  32. optional func pagerView(_ pagerView: FSPagerView, shouldSelectItemAt index: Int) -> Bool
  33. /// Tells the delegate that the item at the specified index was selected.
  34. @objc(pagerView:didSelectItemAtIndex:)
  35. optional func pagerView(_ pagerView: FSPagerView, didSelectItemAt index: Int)
  36. /// Tells the delegate that the specified cell is about to be displayed in the pager view.
  37. @objc(pagerView:willDisplayCell:forItemAtIndex:)
  38. optional func pagerView(_ pagerView: FSPagerView, willDisplay cell: FSPagerViewCell, forItemAt index: Int)
  39. /// Tells the delegate that the specified cell was removed from the pager view.
  40. @objc(pagerView:didEndDisplayingCell:forItemAtIndex:)
  41. optional func pagerView(_ pagerView: FSPagerView, didEndDisplaying cell: FSPagerViewCell, forItemAt index: Int)
  42. /// Tells the delegate when the pager view is about to start scrolling the content.
  43. @objc(pagerViewWillBeginDragging:)
  44. optional func pagerViewWillBeginDragging(_ pagerView: FSPagerView)
  45. /// Tells the delegate when the user finishes scrolling the content.
  46. @objc(pagerViewWillEndDragging:targetIndex:)
  47. optional func pagerViewWillEndDragging(_ pagerView: FSPagerView, targetIndex: Int)
  48. /// Tells the delegate when the user scrolls the content view within the receiver.
  49. @objc(pagerViewDidScroll:)
  50. optional func pagerViewDidScroll(_ pagerView: FSPagerView)
  51. /// Tells the delegate when a scrolling animation in the pager view concludes.
  52. @objc(pagerViewDidEndScrollAnimation:)
  53. optional func pagerViewDidEndScrollAnimation(_ pagerView: FSPagerView)
  54. /// Tells the delegate that the pager view has ended decelerating the scrolling movement.
  55. @objc(pagerViewDidEndDecelerating:)
  56. optional func pagerViewDidEndDecelerating(_ pagerView: FSPagerView)
  57. }
  58. @IBDesignable
  59. open class FSPagerView: UIView,UICollectionViewDataSource,UICollectionViewDelegate {
  60. // MARK: - Public properties
  61. /// The object that acts as the data source of the pager view.
  62. @IBOutlet open weak var dataSource: FSPagerViewDataSource?
  63. /// The object that acts as the delegate of the pager view.
  64. @IBOutlet open weak var delegate: FSPagerViewDelegate?
  65. /// The scroll direction of the pager view. Default is horizontal.
  66. @objc
  67. open var scrollDirection: FSPagerView.ScrollDirection = .horizontal {
  68. didSet {
  69. self.collectionViewLayout.forceInvalidate()
  70. }
  71. }
  72. /// The time interval of automatic sliding. 0 means disabling automatic sliding. Default is 0.
  73. @IBInspectable
  74. open var automaticSlidingInterval: CGFloat = 0.0 {
  75. didSet {
  76. self.cancelTimer()
  77. if self.automaticSlidingInterval > 0 {
  78. self.startTimer()
  79. }
  80. }
  81. }
  82. /// The spacing to use between items in the pager view. Default is 0.
  83. @IBInspectable
  84. open var interitemSpacing: CGFloat = 0 {
  85. didSet {
  86. self.collectionViewLayout.forceInvalidate()
  87. }
  88. }
  89. /// The item size of the pager view. When the value of this property is FSPagerView.automaticSize, the items fill the entire visible area of the pager view. Default is FSPagerView.automaticSize.
  90. @IBInspectable
  91. open var itemSize: CGSize = automaticSize {
  92. didSet {
  93. self.collectionViewLayout.forceInvalidate()
  94. }
  95. }
  96. /// A Boolean value indicates that whether the pager view has infinite items. Default is false.
  97. @IBInspectable
  98. open var isInfinite: Bool = false {
  99. didSet {
  100. self.collectionViewLayout.needsReprepare = true
  101. self.collectionView.reloadData()
  102. }
  103. }
  104. /// An unsigned integer value that determines the deceleration distance of the pager view, which indicates the number of passing items during the deceleration. When the value of this property is FSPagerView.automaticDistance, the actual 'distance' is automatically calculated according to the scrolling speed of the pager view. Default is 1.
  105. @IBInspectable
  106. open var decelerationDistance: UInt = 1
  107. /// A Boolean value that determines whether scrolling is enabled.
  108. @IBInspectable
  109. open var isScrollEnabled: Bool {
  110. set { self.collectionView.isScrollEnabled = newValue }
  111. get { return self.collectionView.isScrollEnabled }
  112. }
  113. /// A Boolean value that controls whether the pager view bounces past the edge of content and back again.
  114. @IBInspectable
  115. open var bounces: Bool {
  116. set { self.collectionView.bounces = newValue }
  117. get { return self.collectionView.bounces }
  118. }
  119. /// A Boolean value that determines whether bouncing always occurs when horizontal scrolling reaches the end of the content view.
  120. @IBInspectable
  121. open var alwaysBounceHorizontal: Bool {
  122. set { self.collectionView.alwaysBounceHorizontal = newValue }
  123. get { return self.collectionView.alwaysBounceHorizontal }
  124. }
  125. /// A Boolean value that determines whether bouncing always occurs when vertical scrolling reaches the end of the content view.
  126. @IBInspectable
  127. open var alwaysBounceVertical: Bool {
  128. set { self.collectionView.alwaysBounceVertical = newValue }
  129. get { return self.collectionView.alwaysBounceVertical }
  130. }
  131. /// A Boolean value that controls whether the infinite loop is removed if there is only one item. Default is false.
  132. @IBInspectable
  133. open var removesInfiniteLoopForSingleItem: Bool = false {
  134. didSet {
  135. self.reloadData()
  136. }
  137. }
  138. /// The background view of the pager view.
  139. @IBInspectable
  140. open var backgroundView: UIView? {
  141. didSet {
  142. if let backgroundView = self.backgroundView {
  143. if backgroundView.superview != nil {
  144. backgroundView.removeFromSuperview()
  145. }
  146. self.insertSubview(backgroundView, at: 0)
  147. self.setNeedsLayout()
  148. }
  149. }
  150. }
  151. /// The transformer of the pager view.
  152. @objc
  153. open var transformer: FSPagerViewTransformer? {
  154. didSet {
  155. self.transformer?.pagerView = self
  156. self.collectionViewLayout.forceInvalidate()
  157. }
  158. }
  159. // MARK: - Public readonly-properties
  160. /// Returns whether the user has touched the content to initiate scrolling.
  161. @objc
  162. open var isTracking: Bool {
  163. return self.collectionView.isTracking
  164. }
  165. /// The percentage of x position at which the origin of the content view is offset from the origin of the pagerView view.
  166. @objc
  167. open var scrollOffset: CGFloat {
  168. let contentOffset = max(self.collectionView.contentOffset.x, self.collectionView.contentOffset.y)
  169. let scrollOffset = Double(contentOffset/self.collectionViewLayout.itemSpacing)
  170. return fmod(CGFloat(scrollOffset), CGFloat(self.numberOfItems))
  171. }
  172. /// The underlying gesture recognizer for pan gestures.
  173. @objc
  174. open var panGestureRecognizer: UIPanGestureRecognizer {
  175. return self.collectionView.panGestureRecognizer
  176. }
  177. @objc open fileprivate(set) dynamic var currentIndex: Int = 0
  178. // MARK: - Private properties
  179. internal weak var collectionViewLayout: FSPagerViewLayout!
  180. internal weak var collectionView: FSPagerCollectionView!
  181. internal weak var contentView: UIView!
  182. internal var timer: Timer?
  183. internal var numberOfItems: Int = 0
  184. internal var numberOfSections: Int = 0
  185. fileprivate var dequeingSection = 0
  186. fileprivate var centermostIndexPath: IndexPath {
  187. guard self.numberOfItems > 0, self.collectionView.contentSize != .zero else {
  188. return IndexPath(item: 0, section: 0)
  189. }
  190. let sortedIndexPaths = self.collectionView.indexPathsForVisibleItems.sorted { (l, r) -> Bool in
  191. let leftFrame = self.collectionViewLayout.frame(for: l)
  192. let rightFrame = self.collectionViewLayout.frame(for: r)
  193. var leftCenter: CGFloat,rightCenter: CGFloat,ruler: CGFloat
  194. switch self.scrollDirection {
  195. case .horizontal:
  196. leftCenter = leftFrame.midX
  197. rightCenter = rightFrame.midX
  198. ruler = self.collectionView.bounds.midX
  199. case .vertical:
  200. leftCenter = leftFrame.midY
  201. rightCenter = rightFrame.midY
  202. ruler = self.collectionView.bounds.midY
  203. }
  204. return abs(ruler-leftCenter) < abs(ruler-rightCenter)
  205. }
  206. let indexPath = sortedIndexPaths.first
  207. if let indexPath = indexPath {
  208. return indexPath
  209. }
  210. return IndexPath(item: 0, section: 0)
  211. }
  212. fileprivate var isPossiblyRotating: Bool {
  213. guard let animationKeys = self.contentView.layer.animationKeys() else {
  214. return false
  215. }
  216. let rotationAnimationKeys = ["position", "bounds.origin", "bounds.size"]
  217. return animationKeys.contains(where: { rotationAnimationKeys.contains($0) })
  218. }
  219. fileprivate var possibleTargetingIndexPath: IndexPath?
  220. // MARK: - Overriden functions
  221. public override init(frame: CGRect) {
  222. super.init(frame: frame)
  223. self.commonInit()
  224. }
  225. public required init?(coder aDecoder: NSCoder) {
  226. super.init(coder: aDecoder)
  227. self.commonInit()
  228. }
  229. open override func layoutSubviews() {
  230. super.layoutSubviews()
  231. self.backgroundView?.frame = self.bounds
  232. self.contentView.frame = self.bounds
  233. self.collectionView.frame = self.contentView.bounds
  234. }
  235. open override func willMove(toWindow newWindow: UIWindow?) {
  236. super.willMove(toWindow: newWindow)
  237. if newWindow != nil {
  238. self.startTimer()
  239. } else {
  240. self.cancelTimer()
  241. }
  242. }
  243. #if TARGET_INTERFACE_BUILDER
  244. open override func prepareForInterfaceBuilder() {
  245. super.prepareForInterfaceBuilder()
  246. self.contentView.layer.borderWidth = 1
  247. self.contentView.layer.cornerRadius = 5
  248. self.contentView.layer.masksToBounds = true
  249. self.contentView.frame = self.bounds
  250. let label = UILabel(frame: self.contentView.bounds)
  251. label.textAlignment = .center
  252. label.font = UIFont.boldSystemFont(ofSize: 25)
  253. label.text = "FSPagerView"
  254. self.contentView.addSubview(label)
  255. }
  256. #endif
  257. deinit {
  258. self.collectionView.dataSource = nil
  259. self.collectionView.delegate = nil
  260. }
  261. // MARK: - UICollectionViewDataSource
  262. public func numberOfSections(in collectionView: UICollectionView) -> Int {
  263. guard let dataSource = self.dataSource else {
  264. return 1
  265. }
  266. self.numberOfItems = dataSource.numberOfItems(in: self)
  267. guard self.numberOfItems > 0 else {
  268. return 0;
  269. }
  270. self.numberOfSections = self.isInfinite && (self.numberOfItems > 1 || !self.removesInfiniteLoopForSingleItem) ? Int(Int16.max)/self.numberOfItems : 1
  271. return self.numberOfSections
  272. }
  273. public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  274. return self.numberOfItems
  275. }
  276. public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  277. let index = indexPath.item
  278. self.dequeingSection = indexPath.section
  279. let cell = self.dataSource!.pagerView(self, cellForItemAt: index)
  280. return cell
  281. }
  282. // MARK: - UICollectionViewDelegate
  283. public func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool {
  284. guard let function = self.delegate?.pagerView(_:shouldHighlightItemAt:) else {
  285. return true
  286. }
  287. let index = indexPath.item % self.numberOfItems
  288. return function(self,index)
  289. }
  290. public func collectionView(_ collectionView: UICollectionView, didHighlightItemAt indexPath: IndexPath) {
  291. guard let function = self.delegate?.pagerView(_:didHighlightItemAt:) else {
  292. return
  293. }
  294. let index = indexPath.item % self.numberOfItems
  295. function(self,index)
  296. }
  297. public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
  298. guard let function = self.delegate?.pagerView(_:shouldSelectItemAt:) else {
  299. return true
  300. }
  301. let index = indexPath.item % self.numberOfItems
  302. return function(self,index)
  303. }
  304. public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  305. guard let function = self.delegate?.pagerView(_:didSelectItemAt:) else {
  306. return
  307. }
  308. self.possibleTargetingIndexPath = indexPath
  309. defer {
  310. self.possibleTargetingIndexPath = nil
  311. }
  312. let index = indexPath.item % self.numberOfItems
  313. function(self,index)
  314. }
  315. public func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  316. guard let function = self.delegate?.pagerView(_:willDisplay:forItemAt:) else {
  317. return
  318. }
  319. let index = indexPath.item % self.numberOfItems
  320. function(self,cell as! FSPagerViewCell,index)
  321. }
  322. public func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
  323. guard let function = self.delegate?.pagerView(_:didEndDisplaying:forItemAt:) else {
  324. return
  325. }
  326. let index = indexPath.item % self.numberOfItems
  327. function(self,cell as! FSPagerViewCell,index)
  328. }
  329. public func scrollViewDidScroll(_ scrollView: UIScrollView) {
  330. if !self.isPossiblyRotating && self.numberOfItems > 0 {
  331. // In case someone is using KVO
  332. let currentIndex = lround(Double(self.scrollOffset)) % self.numberOfItems
  333. if (currentIndex != self.currentIndex) {
  334. self.currentIndex = currentIndex
  335. }
  336. }
  337. guard let function = self.delegate?.pagerViewDidScroll else {
  338. return
  339. }
  340. function(self)
  341. }
  342. public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  343. if let function = self.delegate?.pagerViewWillBeginDragging(_:) {
  344. function(self)
  345. }
  346. if self.automaticSlidingInterval > 0 {
  347. self.cancelTimer()
  348. }
  349. }
  350. public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
  351. if let function = self.delegate?.pagerViewWillEndDragging(_:targetIndex:) {
  352. let contentOffset = self.scrollDirection == .horizontal ? targetContentOffset.pointee.x : targetContentOffset.pointee.y
  353. let targetItem = lround(Double(contentOffset/self.collectionViewLayout.itemSpacing))
  354. function(self, targetItem % self.numberOfItems)
  355. }
  356. if self.automaticSlidingInterval > 0 {
  357. self.startTimer()
  358. }
  359. }
  360. public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
  361. if let function = self.delegate?.pagerViewDidEndDecelerating {
  362. function(self)
  363. }
  364. }
  365. public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  366. if let function = self.delegate?.pagerViewDidEndScrollAnimation {
  367. function(self)
  368. }
  369. }
  370. // MARK: - Public functions
  371. /// Register a class for use in creating new pager view cells.
  372. ///
  373. /// - Parameters:
  374. /// - cellClass: The class of a cell that you want to use in the pager view.
  375. /// - identifier: The reuse identifier to associate with the specified class. This parameter must not be nil and must not be an empty string.
  376. @objc(registerClass:forCellWithReuseIdentifier:)
  377. open func register(_ cellClass: Swift.AnyClass?, forCellWithReuseIdentifier identifier: String) {
  378. self.collectionView.register(cellClass, forCellWithReuseIdentifier: identifier)
  379. }
  380. /// Register a nib file for use in creating new pager view cells.
  381. ///
  382. /// - Parameters:
  383. /// - nib: The nib object containing the cell object. The nib file must contain only one top-level object and that object must be of the type FSPagerViewCell.
  384. /// - identifier: The reuse identifier to associate with the specified nib file. This parameter must not be nil and must not be an empty string.
  385. @objc(registerNib:forCellWithReuseIdentifier:)
  386. open func register(_ nib: UINib?, forCellWithReuseIdentifier identifier: String) {
  387. self.collectionView.register(nib, forCellWithReuseIdentifier: identifier)
  388. }
  389. /// Returns a reusable cell object located by its identifier
  390. ///
  391. /// - Parameters:
  392. /// - identifier: The reuse identifier for the specified cell. This parameter must not be nil.
  393. /// - index: The index specifying the location of the cell.
  394. /// - Returns: A valid FSPagerViewCell object.
  395. @objc(dequeueReusableCellWithReuseIdentifier:atIndex:)
  396. open func dequeueReusableCell(withReuseIdentifier identifier: String, at index: Int) -> FSPagerViewCell {
  397. let indexPath = IndexPath(item: index, section: self.dequeingSection)
  398. let cell = self.collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)
  399. guard cell.isKind(of: FSPagerViewCell.self) else {
  400. fatalError("Cell class must be subclass of FSPagerViewCell")
  401. }
  402. return cell as! FSPagerViewCell
  403. }
  404. /// Reloads all of the data for the collection view.
  405. @objc(reloadData)
  406. open func reloadData() {
  407. self.collectionViewLayout.needsReprepare = true;
  408. self.collectionView.reloadData()
  409. }
  410. /// Selects the item at the specified index and optionally scrolls it into view.
  411. ///
  412. /// - Parameters:
  413. /// - index: The index path of the item to select.
  414. /// - animated: Specify true to animate the change in the selection or false to make the change without animating it.
  415. @objc(selectItemAtIndex:animated:)
  416. open func selectItem(at index: Int, animated: Bool) {
  417. let indexPath = self.nearbyIndexPath(for: index)
  418. let scrollPosition: UICollectionView.ScrollPosition = self.scrollDirection == .horizontal ? .centeredHorizontally : .centeredVertically
  419. self.collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: scrollPosition)
  420. }
  421. /// Deselects the item at the specified index.
  422. ///
  423. /// - Parameters:
  424. /// - index: The index of the item to deselect.
  425. /// - animated: Specify true to animate the change in the selection or false to make the change without animating it.
  426. @objc(deselectItemAtIndex:animated:)
  427. open func deselectItem(at index: Int, animated: Bool) {
  428. let indexPath = self.nearbyIndexPath(for: index)
  429. self.collectionView.deselectItem(at: indexPath, animated: animated)
  430. }
  431. /// Scrolls the pager view contents until the specified item is visible.
  432. ///
  433. /// - Parameters:
  434. /// - index: The index of the item to scroll into view.
  435. /// - animated: Specify true to animate the scrolling behavior or false to adjust the pager view’s visible content immediately.
  436. @objc(scrollToItemAtIndex:animated:)
  437. open func scrollToItem(at index: Int, animated: Bool) {
  438. guard index < self.numberOfItems else {
  439. fatalError("index \(index) is out of range [0...\(self.numberOfItems-1)]")
  440. }
  441. let indexPath = { () -> IndexPath in
  442. if let indexPath = self.possibleTargetingIndexPath, indexPath.item == index {
  443. defer {
  444. self.possibleTargetingIndexPath = nil
  445. }
  446. return indexPath
  447. }
  448. return self.numberOfSections > 1 ? self.nearbyIndexPath(for: index) : IndexPath(item: index, section: 0)
  449. }()
  450. let contentOffset = self.collectionViewLayout.contentOffset(for: indexPath)
  451. self.collectionView.setContentOffset(contentOffset, animated: animated)
  452. }
  453. /// Returns the index of the specified cell.
  454. ///
  455. /// - Parameter cell: The cell object whose index you want.
  456. /// - Returns: The index of the cell or NSNotFound if the specified cell is not in the pager view.
  457. @objc(indexForCell:)
  458. open func index(for cell: FSPagerViewCell) -> Int {
  459. guard let indexPath = self.collectionView.indexPath(for: cell) else {
  460. return NSNotFound
  461. }
  462. return indexPath.item
  463. }
  464. /// Returns the visible cell at the specified index.
  465. ///
  466. /// - Parameter index: The index that specifies the position of the cell.
  467. /// - Returns: The cell object at the corresponding position or nil if the cell is not visible or index is out of range.
  468. @objc(cellForItemAtIndex:)
  469. open func cellForItem(at index: Int) -> FSPagerViewCell? {
  470. let indexPath = self.nearbyIndexPath(for: index)
  471. return self.collectionView.cellForItem(at: indexPath) as? FSPagerViewCell
  472. }
  473. // MARK: - Private functions
  474. fileprivate func commonInit() {
  475. // Content View
  476. let contentView = UIView(frame:CGRect.zero)
  477. contentView.backgroundColor = UIColor.clear
  478. self.addSubview(contentView)
  479. self.contentView = contentView
  480. // UICollectionView
  481. let collectionViewLayout = FSPagerViewLayout()
  482. let collectionView = FSPagerCollectionView(frame: CGRect.zero, collectionViewLayout: collectionViewLayout)
  483. collectionView.dataSource = self
  484. collectionView.delegate = self
  485. collectionView.backgroundColor = UIColor.clear
  486. self.contentView.addSubview(collectionView)
  487. self.collectionView = collectionView
  488. self.collectionViewLayout = collectionViewLayout
  489. }
  490. fileprivate func startTimer() {
  491. guard self.automaticSlidingInterval > 0 && self.timer == nil else {
  492. return
  493. }
  494. self.timer = Timer.scheduledTimer(timeInterval: TimeInterval(self.automaticSlidingInterval), target: self, selector: #selector(self.flipNext(sender:)), userInfo: nil, repeats: true)
  495. RunLoop.current.add(self.timer!, forMode: .common)
  496. }
  497. @objc
  498. fileprivate func flipNext(sender: Timer?) {
  499. guard let _ = self.superview, let _ = self.window, self.numberOfItems > 0, !self.isTracking else {
  500. return
  501. }
  502. let contentOffset: CGPoint = {
  503. let indexPath = self.centermostIndexPath
  504. let section = self.numberOfSections > 1 ? (indexPath.section+(indexPath.item+1)/self.numberOfItems) : 0
  505. let item = (indexPath.item+1) % self.numberOfItems
  506. return self.collectionViewLayout.contentOffset(for: IndexPath(item: item, section: section))
  507. }()
  508. self.collectionView.setContentOffset(contentOffset, animated: true)
  509. }
  510. fileprivate func cancelTimer() {
  511. guard self.timer != nil else {
  512. return
  513. }
  514. self.timer!.invalidate()
  515. self.timer = nil
  516. }
  517. fileprivate func nearbyIndexPath(for index: Int) -> IndexPath {
  518. // Is there a better algorithm?
  519. let currentIndex = self.currentIndex
  520. let currentSection = self.centermostIndexPath.section
  521. if abs(currentIndex-index) <= self.numberOfItems/2 {
  522. return IndexPath(item: index, section: currentSection)
  523. } else if (index-currentIndex >= 0) {
  524. return IndexPath(item: index, section: currentSection-1)
  525. } else {
  526. return IndexPath(item: index, section: currentSection+1)
  527. }
  528. }
  529. }
  530. extension FSPagerView {
  531. /// Constants indicating the direction of scrolling for the pager view.
  532. @objc
  533. public enum ScrollDirection: Int {
  534. /// The pager view scrolls content horizontally
  535. case horizontal
  536. /// The pager view scrolls content vertically
  537. case vertical
  538. }
  539. /// Requests that FSPagerView use the default value for a given distance.
  540. public static let automaticDistance: UInt = 0
  541. /// Requests that FSPagerView use the default value for a given size.
  542. public static let automaticSize: CGSize = .zero
  543. }