PagerStripViewController.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /*****************************************************************************
  2. * PagerTabStripViewController.Swift
  3. * VLC for iOS
  4. *****************************************************************************
  5. * Copyright (c) 2018 VideoLAN. All rights reserved.
  6. * $Id$
  7. *
  8. * Authors: Carola Nitz <nitz.carola # googlemail.com>
  9. *
  10. * Refer to the COPYING file of the official project for license.
  11. *****************************************************************************/
  12. import Foundation
  13. open class PagerTabStripViewController: UIViewController, UIScrollViewDelegate {
  14. public var containerView: UIScrollView!
  15. open weak var delegate: PagerTabStripIsProgressiveDelegate?
  16. open weak var datasource: PagerTabStripDataSource?
  17. open private(set) var viewControllers = [UIViewController]()
  18. open private(set) var currentIndex = 0
  19. open private(set) var preCurrentIndex = 0 // used *only* to store the index to which move when the pager becomes visible
  20. open var pageWidth: CGFloat {
  21. return containerView.bounds.width
  22. }
  23. open var scrollPercentage: CGFloat {
  24. if swipeDirection != .right {
  25. let module = fmod(containerView.contentOffset.x, pageWidth)
  26. return module == 0.0 ? 1.0 : module / pageWidth
  27. }
  28. return 1 - fmod(containerView.contentOffset.x >= 0 ? containerView.contentOffset.x : pageWidth + containerView.contentOffset.x, pageWidth) / pageWidth
  29. }
  30. open var swipeDirection: SwipeDirection {
  31. if containerView.contentOffset.x > lastContentOffset {
  32. return .left
  33. } else if containerView.contentOffset.x < lastContentOffset {
  34. return .right
  35. }
  36. return .none
  37. }
  38. override open func viewDidLoad() {
  39. super.viewDidLoad()
  40. containerView = UIScrollView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height))
  41. containerView.translatesAutoresizingMaskIntoConstraints = false
  42. containerView.bounces = true
  43. containerView.alwaysBounceHorizontal = true
  44. containerView.alwaysBounceVertical = false
  45. containerView.scrollsToTop = false
  46. containerView.delegate = self
  47. containerView.showsVerticalScrollIndicator = false
  48. containerView.showsHorizontalScrollIndicator = false
  49. containerView.isPagingEnabled = true
  50. containerView.backgroundColor = PresentationTheme.current.colors.background
  51. view.addSubview(containerView)
  52. reloadViewControllers()
  53. let childController = viewControllers[currentIndex]
  54. addChildViewController(childController)
  55. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  56. containerView.addSubview(childController.view)
  57. childController.didMove(toParentViewController: self)
  58. }
  59. open override func viewWillAppear(_ animated: Bool) {
  60. super.viewWillAppear(animated)
  61. isViewAppearing = true
  62. childViewControllers.forEach { $0.beginAppearanceTransition(true, animated: animated) }
  63. }
  64. override open func viewDidAppear(_ animated: Bool) {
  65. super.viewDidAppear(animated)
  66. lastSize = containerView.bounds.size
  67. updateIfNeeded()
  68. let needToUpdateCurrentChild = preCurrentIndex != currentIndex
  69. if needToUpdateCurrentChild {
  70. moveToViewController(at: preCurrentIndex)
  71. }
  72. isViewAppearing = false
  73. childViewControllers.forEach { $0.endAppearanceTransition() }
  74. }
  75. open override func viewWillDisappear(_ animated: Bool) {
  76. super.viewWillDisappear(animated)
  77. childViewControllers.forEach { $0.beginAppearanceTransition(false, animated: animated) }
  78. }
  79. open override func viewDidDisappear(_ animated: Bool) {
  80. super.viewDidDisappear(animated)
  81. childViewControllers.forEach { $0.endAppearanceTransition() }
  82. }
  83. override open func viewDidLayoutSubviews() {
  84. super.viewDidLayoutSubviews()
  85. updateIfNeeded()
  86. }
  87. open override var shouldAutomaticallyForwardAppearanceMethods: Bool {
  88. return false
  89. }
  90. open func moveToViewController(at index: Int, animated: Bool = true) {
  91. guard isViewLoaded && view.window != nil && currentIndex != index else {
  92. preCurrentIndex = index
  93. return
  94. }
  95. if animated && abs(currentIndex - index) > 1 {
  96. var tmpViewControllers = viewControllers
  97. let currentChildVC = viewControllers[currentIndex]
  98. let fromIndex = currentIndex < index ? index - 1 : index + 1
  99. let fromChildVC = viewControllers[fromIndex]
  100. tmpViewControllers[currentIndex] = fromChildVC
  101. tmpViewControllers[fromIndex] = currentChildVC
  102. pagerTabStripChildViewControllersForScrolling = tmpViewControllers
  103. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: fromIndex), y: 0), animated: false)
  104. (navigationController?.view ?? view).isUserInteractionEnabled = !animated
  105. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: true)
  106. } else {
  107. (navigationController?.view ?? view).isUserInteractionEnabled = !animated
  108. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: animated)
  109. }
  110. }
  111. open func moveTo(viewController: UIViewController, animated: Bool = true) {
  112. moveToViewController(at: viewControllers.index(of: viewController)!, animated: animated)
  113. }
  114. // MARK: - PagerTabStripDataSource
  115. open func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
  116. assertionFailure("Sub-class must implement the PagerTabStripDataSource viewControllers(for:) method")
  117. return []
  118. }
  119. // MARK: - Helpers
  120. open func updateIfNeeded() {
  121. if isViewLoaded && !lastSize.equalTo(containerView.bounds.size) {
  122. updateContent()
  123. }
  124. }
  125. open func canMoveTo(index: Int) -> Bool {
  126. return currentIndex != index && viewControllers.count > index
  127. }
  128. open func pageOffsetForChild(at index: Int) -> CGFloat {
  129. return CGFloat(index) * containerView.bounds.width
  130. }
  131. open func offsetForChild(at index: Int) -> CGFloat {
  132. return (CGFloat(index) * containerView.bounds.width) + ((containerView.bounds.width - view.bounds.width) * 0.5)
  133. }
  134. public enum PagerTabStripError: Error {
  135. case viewControllerOutOfBounds
  136. }
  137. open func offsetForChild(viewController: UIViewController) throws -> CGFloat {
  138. guard let index = viewControllers.index(of: viewController) else {
  139. throw PagerTabStripError.viewControllerOutOfBounds
  140. }
  141. return offsetForChild(at: index)
  142. }
  143. open func pageFor(contentOffset: CGFloat) -> Int {
  144. let result = virtualPageFor(contentOffset: contentOffset)
  145. return pageFor(virtualPage: result)
  146. }
  147. open func virtualPageFor(contentOffset: CGFloat) -> Int {
  148. return Int((contentOffset + 1.5 * pageWidth) / pageWidth) - 1
  149. }
  150. open func pageFor(virtualPage: Int) -> Int {
  151. if virtualPage < 0 {
  152. return 0
  153. }
  154. if virtualPage > viewControllers.count - 1 {
  155. return viewControllers.count - 1
  156. }
  157. return virtualPage
  158. }
  159. open func updateContent() {
  160. if lastSize.width != containerView.bounds.size.width {
  161. lastSize = containerView.bounds.size
  162. containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
  163. }
  164. lastSize = containerView.bounds.size
  165. let pagerViewControllers = pagerTabStripChildViewControllersForScrolling ?? viewControllers
  166. containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(pagerViewControllers.count), height: containerView.contentSize.height)
  167. for (index, childController) in pagerViewControllers.enumerated() {
  168. let pageOffsetForChild = self.pageOffsetForChild(at: index)
  169. if fabs(containerView.contentOffset.x - pageOffsetForChild) < containerView.bounds.width {
  170. if childController.parent != nil {
  171. childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
  172. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  173. } else {
  174. childController.beginAppearanceTransition(true, animated: false)
  175. addChildViewController(childController)
  176. childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
  177. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  178. containerView.addSubview(childController.view)
  179. childController.didMove(toParentViewController: self)
  180. childController.endAppearanceTransition()
  181. }
  182. } else {
  183. if childController.parent != nil {
  184. childController.beginAppearanceTransition(false, animated: false)
  185. childController.willMove(toParentViewController: nil)
  186. childController.view.removeFromSuperview()
  187. childController.removeFromParentViewController()
  188. childController.endAppearanceTransition()
  189. }
  190. }
  191. }
  192. let oldCurrentIndex = currentIndex
  193. let virtualPage = virtualPageFor(contentOffset: containerView.contentOffset.x)
  194. let newCurrentIndex = pageFor(virtualPage: virtualPage)
  195. currentIndex = newCurrentIndex
  196. preCurrentIndex = currentIndex
  197. let changeCurrentIndex = newCurrentIndex != oldCurrentIndex
  198. if let progressiveDelegate = self as? PagerTabStripIsProgressiveDelegate {
  199. let (fromIndex, toIndex, scrollPercentage) = progressiveIndicatorData(virtualPage)
  200. progressiveDelegate.updateIndicator(for: self, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: scrollPercentage, indexWasChanged: changeCurrentIndex)
  201. }
  202. }
  203. open func reloadPagerTabStripView() {
  204. guard isViewLoaded else { return }
  205. for childController in viewControllers where childController.parent != nil {
  206. childController.beginAppearanceTransition(false, animated: false)
  207. childController.willMove(toParentViewController: nil)
  208. childController.view.removeFromSuperview()
  209. childController.removeFromParentViewController()
  210. childController.endAppearanceTransition()
  211. }
  212. reloadViewControllers()
  213. containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(viewControllers.count), height: containerView.contentSize.height)
  214. if currentIndex >= viewControllers.count {
  215. currentIndex = viewControllers.count - 1
  216. }
  217. preCurrentIndex = currentIndex
  218. containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
  219. updateContent()
  220. }
  221. // MARK: - UIScrollViewDelegate
  222. open func scrollViewDidScroll(_ scrollView: UIScrollView) {
  223. if containerView == scrollView {
  224. updateContent()
  225. lastContentOffset = scrollView.contentOffset.x
  226. }
  227. }
  228. open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  229. if containerView == scrollView {
  230. lastPageNumber = pageFor(contentOffset: scrollView.contentOffset.x)
  231. }
  232. }
  233. open func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  234. if containerView == scrollView {
  235. pagerTabStripChildViewControllersForScrolling = nil
  236. (navigationController?.view ?? view).isUserInteractionEnabled = true
  237. updateContent()
  238. }
  239. }
  240. // MARK: - Orientation
  241. open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  242. super.viewWillTransition(to: size, with: coordinator)
  243. isViewRotating = true
  244. pageBeforeRotate = currentIndex
  245. coordinator.animate(alongsideTransition: nil) { [weak self] _ in
  246. guard let me = self else { return }
  247. me.isViewRotating = false
  248. me.currentIndex = me.pageBeforeRotate
  249. me.preCurrentIndex = me.currentIndex
  250. me.updateIfNeeded()
  251. }
  252. }
  253. // MARK: Private
  254. private func progressiveIndicatorData(_ virtualPage: Int) -> (Int, Int, CGFloat) {
  255. let count = viewControllers.count
  256. var fromIndex = currentIndex
  257. var toIndex = currentIndex
  258. let direction = swipeDirection
  259. if direction == .left {
  260. if virtualPage > count - 1 {
  261. fromIndex = count - 1
  262. toIndex = count
  263. } else {
  264. if self.scrollPercentage >= 0.5 {
  265. fromIndex = max(toIndex - 1, 0)
  266. } else {
  267. toIndex = fromIndex + 1
  268. }
  269. }
  270. } else if direction == .right {
  271. if virtualPage < 0 {
  272. fromIndex = 0
  273. toIndex = -1
  274. } else {
  275. if self.scrollPercentage > 0.5 {
  276. fromIndex = min(toIndex + 1, count - 1)
  277. } else {
  278. toIndex = fromIndex - 1
  279. }
  280. }
  281. }
  282. return (fromIndex, toIndex, self.scrollPercentage)
  283. }
  284. private func reloadViewControllers() {
  285. guard let dataSource = datasource else {
  286. fatalError("dataSource must not be nil")
  287. }
  288. viewControllers = dataSource.viewControllers(for: self)
  289. guard !viewControllers.isEmpty else {
  290. fatalError("viewControllers(for:) should provide at least one child view controller")
  291. }
  292. viewControllers.forEach { if !($0 is IndicatorInfoProvider) { fatalError("Every view controller provided by PagerTabStripDataSource's viewControllers(for:) method must conform to IndicatorInfoProvider") }}
  293. }
  294. private var pagerTabStripChildViewControllersForScrolling: [UIViewController]?
  295. private var lastPageNumber = 0
  296. private var lastContentOffset: CGFloat = 0.0
  297. private var pageBeforeRotate = 0
  298. private var lastSize = CGSize(width: 0, height: 0)
  299. internal var isViewRotating = false
  300. internal var isViewAppearing = false
  301. }