PagerStripViewController.swift 14 KB

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