You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

396 lines
16 KiB

6 years ago
  1. // PagerTabStripViewController.swift
  2. // XLPagerTabStrip ( https://github.com/xmartlabs/XLPagerTabStrip )
  3. //
  4. // Copyright (c) 2017 Xmartlabs ( http://xmartlabs.com )
  5. //
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. import Foundation
  25. // MARK: Protocols
  26. public protocol IndicatorInfoProvider {
  27. func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo
  28. }
  29. public protocol PagerTabStripDelegate: class {
  30. func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int)
  31. }
  32. public protocol PagerTabStripIsProgressiveDelegate: PagerTabStripDelegate {
  33. func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool)
  34. }
  35. public protocol PagerTabStripDataSource: class {
  36. func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController]
  37. }
  38. // MARK: PagerTabStripViewController
  39. open class PagerTabStripViewController: UIViewController, UIScrollViewDelegate {
  40. @IBOutlet weak public var containerView: UIScrollView!
  41. open weak var delegate: PagerTabStripDelegate?
  42. open weak var datasource: PagerTabStripDataSource?
  43. open var pagerBehaviour = PagerTabStripBehaviour.progressive(skipIntermediateViewControllers: true, elasticIndicatorLimit: true)
  44. open private(set) var viewControllers = [UIViewController]()
  45. open private(set) var currentIndex = 0
  46. open private(set) var preCurrentIndex = 0 // used *only* to store the index to which move when the pager becomes visible
  47. open var pageWidth: CGFloat {
  48. return containerView.bounds.width
  49. }
  50. open var scrollPercentage: CGFloat {
  51. if swipeDirection != .right {
  52. let module = fmod(containerView.contentOffset.x, pageWidth)
  53. return module == 0.0 ? 1.0 : module / pageWidth
  54. }
  55. return 1 - fmod(containerView.contentOffset.x >= 0 ? containerView.contentOffset.x : pageWidth + containerView.contentOffset.x, pageWidth) / pageWidth
  56. }
  57. open var swipeDirection: SwipeDirection {
  58. if containerView.contentOffset.x > lastContentOffset {
  59. return .left
  60. } else if containerView.contentOffset.x < lastContentOffset {
  61. return .right
  62. }
  63. return .none
  64. }
  65. override open func viewDidLoad() {
  66. super.viewDidLoad()
  67. let conteinerViewAux = containerView ?? {
  68. let containerView = UIScrollView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height))
  69. containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  70. return containerView
  71. }()
  72. containerView = conteinerViewAux
  73. if containerView.superview == nil {
  74. view.addSubview(containerView)
  75. }
  76. containerView.bounces = true
  77. containerView.alwaysBounceHorizontal = true
  78. containerView.alwaysBounceVertical = false
  79. containerView.scrollsToTop = false
  80. containerView.delegate = self
  81. containerView.showsVerticalScrollIndicator = false
  82. containerView.showsHorizontalScrollIndicator = false
  83. containerView.isPagingEnabled = true
  84. reloadViewControllers()
  85. let childController = viewControllers[currentIndex]
  86. addChildViewController(childController)
  87. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  88. containerView.addSubview(childController.view)
  89. childController.didMove(toParentViewController: self)
  90. }
  91. open override func viewWillAppear(_ animated: Bool) {
  92. super.viewWillAppear(animated)
  93. isViewAppearing = true
  94. childViewControllers.forEach { $0.beginAppearanceTransition(true, animated: animated) }
  95. }
  96. override open func viewDidAppear(_ animated: Bool) {
  97. super.viewDidAppear(animated)
  98. lastSize = containerView.bounds.size
  99. updateIfNeeded()
  100. let needToUpdateCurrentChild = preCurrentIndex != currentIndex
  101. if needToUpdateCurrentChild {
  102. moveToViewController(at: preCurrentIndex)
  103. }
  104. isViewAppearing = false
  105. childViewControllers.forEach { $0.endAppearanceTransition() }
  106. }
  107. open override func viewWillDisappear(_ animated: Bool) {
  108. super.viewWillDisappear(animated)
  109. childViewControllers.forEach { $0.beginAppearanceTransition(false, animated: animated) }
  110. }
  111. open override func viewDidDisappear(_ animated: Bool) {
  112. super.viewDidDisappear(animated)
  113. childViewControllers.forEach { $0.endAppearanceTransition() }
  114. }
  115. override open func viewDidLayoutSubviews() {
  116. super.viewDidLayoutSubviews()
  117. updateIfNeeded()
  118. }
  119. open override var shouldAutomaticallyForwardAppearanceMethods: Bool {
  120. return false
  121. }
  122. open func moveToViewController(at index: Int, animated: Bool = true) {
  123. guard isViewLoaded && view.window != nil && currentIndex != index else {
  124. preCurrentIndex = index
  125. return
  126. }
  127. if animated && pagerBehaviour.skipIntermediateViewControllers && abs(currentIndex - index) > 1 {
  128. var tmpViewControllers = viewControllers
  129. let currentChildVC = viewControllers[currentIndex]
  130. let fromIndex = currentIndex < index ? index - 1 : index + 1
  131. let fromChildVC = viewControllers[fromIndex]
  132. tmpViewControllers[currentIndex] = fromChildVC
  133. tmpViewControllers[fromIndex] = currentChildVC
  134. pagerTabStripChildViewControllersForScrolling = tmpViewControllers
  135. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: fromIndex), y: 0), animated: false)
  136. (navigationController?.view ?? view).isUserInteractionEnabled = !animated
  137. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: true)
  138. } else {
  139. (navigationController?.view ?? view).isUserInteractionEnabled = !animated
  140. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: animated)
  141. }
  142. }
  143. open func moveTo(viewController: UIViewController, animated: Bool = true) {
  144. moveToViewController(at: viewControllers.index(of: viewController)!, animated: animated)
  145. }
  146. // MARK: - PagerTabStripDataSource
  147. open func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
  148. assertionFailure("Sub-class must implement the PagerTabStripDataSource viewControllers(for:) method")
  149. return []
  150. }
  151. // MARK: - Helpers
  152. open func updateIfNeeded() {
  153. if isViewLoaded && !lastSize.equalTo(containerView.bounds.size) {
  154. updateContent()
  155. }
  156. }
  157. open func canMoveTo(index: Int) -> Bool {
  158. return currentIndex != index && viewControllers.count > index
  159. }
  160. open func pageOffsetForChild(at index: Int) -> CGFloat {
  161. return CGFloat(index) * containerView.bounds.width
  162. }
  163. open func offsetForChild(at index: Int) -> CGFloat {
  164. return (CGFloat(index) * containerView.bounds.width) + ((containerView.bounds.width - view.bounds.width) * 0.5)
  165. }
  166. open func offsetForChild(viewController: UIViewController) throws -> CGFloat {
  167. guard let index = viewControllers.index(of: viewController) else {
  168. throw PagerTabStripError.viewControllerOutOfBounds
  169. }
  170. return offsetForChild(at: index)
  171. }
  172. open func pageFor(contentOffset: CGFloat) -> Int {
  173. let result = virtualPageFor(contentOffset: contentOffset)
  174. return pageFor(virtualPage: result)
  175. }
  176. open func virtualPageFor(contentOffset: CGFloat) -> Int {
  177. return Int((contentOffset + 1.5 * pageWidth) / pageWidth) - 1
  178. }
  179. open func pageFor(virtualPage: Int) -> Int {
  180. if virtualPage < 0 {
  181. return 0
  182. }
  183. if virtualPage > viewControllers.count - 1 {
  184. return viewControllers.count - 1
  185. }
  186. return virtualPage
  187. }
  188. open func updateContent() {
  189. if lastSize.width != containerView.bounds.size.width {
  190. lastSize = containerView.bounds.size
  191. containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
  192. }
  193. lastSize = containerView.bounds.size
  194. let pagerViewControllers = pagerTabStripChildViewControllersForScrolling ?? viewControllers
  195. containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(pagerViewControllers.count), height: containerView.contentSize.height)
  196. for (index, childController) in pagerViewControllers.enumerated() {
  197. let pageOffsetForChild = self.pageOffsetForChild(at: index)
  198. if fabs(containerView.contentOffset.x - pageOffsetForChild) < containerView.bounds.width {
  199. if childController.parent != nil {
  200. childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
  201. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  202. } else {
  203. childController.beginAppearanceTransition(true, animated: false)
  204. addChildViewController(childController)
  205. childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
  206. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  207. containerView.addSubview(childController.view)
  208. childController.didMove(toParentViewController: self)
  209. childController.endAppearanceTransition()
  210. }
  211. } else {
  212. if childController.parent != nil {
  213. childController.beginAppearanceTransition(false, animated: false)
  214. childController.willMove(toParentViewController: nil)
  215. childController.view.removeFromSuperview()
  216. childController.removeFromParentViewController()
  217. childController.endAppearanceTransition()
  218. }
  219. }
  220. }
  221. let oldCurrentIndex = currentIndex
  222. let virtualPage = virtualPageFor(contentOffset: containerView.contentOffset.x)
  223. let newCurrentIndex = pageFor(virtualPage: virtualPage)
  224. currentIndex = newCurrentIndex
  225. preCurrentIndex = currentIndex
  226. let changeCurrentIndex = newCurrentIndex != oldCurrentIndex
  227. if let progressiveDelegate = self as? PagerTabStripIsProgressiveDelegate, pagerBehaviour.isProgressiveIndicator {
  228. let (fromIndex, toIndex, scrollPercentage) = progressiveIndicatorData(virtualPage)
  229. progressiveDelegate.updateIndicator(for: self, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: scrollPercentage, indexWasChanged: changeCurrentIndex)
  230. } else {
  231. delegate?.updateIndicator(for: self, fromIndex: min(oldCurrentIndex, pagerViewControllers.count - 1), toIndex: newCurrentIndex)
  232. }
  233. }
  234. open func reloadPagerTabStripView() {
  235. guard isViewLoaded else { return }
  236. for childController in viewControllers where childController.parent != nil {
  237. childController.beginAppearanceTransition(false, animated: false)
  238. childController.willMove(toParentViewController: nil)
  239. childController.view.removeFromSuperview()
  240. childController.removeFromParentViewController()
  241. childController.endAppearanceTransition()
  242. }
  243. reloadViewControllers()
  244. containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(viewControllers.count), height: containerView.contentSize.height)
  245. if currentIndex >= viewControllers.count {
  246. currentIndex = viewControllers.count - 1
  247. }
  248. preCurrentIndex = currentIndex
  249. containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
  250. updateContent()
  251. }
  252. // MARK: - UIScrollDelegate
  253. open func scrollViewDidScroll(_ scrollView: UIScrollView) {
  254. if containerView == scrollView {
  255. updateContent()
  256. lastContentOffset = scrollView.contentOffset.x
  257. }
  258. }
  259. open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  260. if containerView == scrollView {
  261. lastPageNumber = pageFor(contentOffset: scrollView.contentOffset.x)
  262. }
  263. }
  264. open func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  265. if containerView == scrollView {
  266. pagerTabStripChildViewControllersForScrolling = nil
  267. (navigationController?.view ?? view).isUserInteractionEnabled = true
  268. updateContent()
  269. }
  270. }
  271. // MARK: - Orientation
  272. open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  273. super.viewWillTransition(to: size, with: coordinator)
  274. isViewRotating = true
  275. pageBeforeRotate = currentIndex
  276. coordinator.animate(alongsideTransition: nil) { [weak self] _ in
  277. guard let me = self else { return }
  278. me.isViewRotating = false
  279. me.currentIndex = me.pageBeforeRotate
  280. me.preCurrentIndex = me.currentIndex
  281. me.updateIfNeeded()
  282. }
  283. }
  284. // MARK: Private
  285. private func progressiveIndicatorData(_ virtualPage: Int) -> (Int, Int, CGFloat) {
  286. let count = viewControllers.count
  287. var fromIndex = currentIndex
  288. var toIndex = currentIndex
  289. let direction = swipeDirection
  290. if direction == .left {
  291. if virtualPage > count - 1 {
  292. fromIndex = count - 1
  293. toIndex = count
  294. } else {
  295. if self.scrollPercentage >= 0.5 {
  296. fromIndex = max(toIndex - 1, 0)
  297. } else {
  298. toIndex = fromIndex + 1
  299. }
  300. }
  301. } else if direction == .right {
  302. if virtualPage < 0 {
  303. fromIndex = 0
  304. toIndex = -1
  305. } else {
  306. if self.scrollPercentage > 0.5 {
  307. fromIndex = min(toIndex + 1, count - 1)
  308. } else {
  309. toIndex = fromIndex - 1
  310. }
  311. }
  312. }
  313. let scrollPercentage = pagerBehaviour.isElasticIndicatorLimit ? self.scrollPercentage : ((toIndex < 0 || toIndex >= count) ? 0.0 : self.scrollPercentage)
  314. return (fromIndex, toIndex, scrollPercentage)
  315. }
  316. private func reloadViewControllers() {
  317. guard let dataSource = datasource else {
  318. fatalError("dataSource must not be nil")
  319. }
  320. viewControllers = dataSource.viewControllers(for: self)
  321. // viewControllers
  322. guard !viewControllers.isEmpty else {
  323. fatalError("viewControllers(for:) should provide at least one child view controller")
  324. }
  325. viewControllers.forEach { if !($0 is IndicatorInfoProvider) { fatalError("Every view controller provided by PagerTabStripDataSource's viewControllers(for:) method must conform to IndicatorInfoProvider") }}
  326. }
  327. private var pagerTabStripChildViewControllersForScrolling: [UIViewController]?
  328. private var lastPageNumber = 0
  329. private var lastContentOffset: CGFloat = 0.0
  330. private var pageBeforeRotate = 0
  331. private var lastSize = CGSize(width: 0, height: 0)
  332. internal var isViewRotating = false
  333. internal var isViewAppearing = false
  334. }