BaseButtonBarPagerTabStripViewController.swift 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800
  1. //
  2. // BaseButtonBarPagerTabStripViewController.swift
  3. // VLC-iOS
  4. //
  5. // Created by Carola Nitz on 5/3/18.
  6. // Copyright © 2018 VideoLAN. All rights reserved.
  7. //
  8. import Foundation
  9. class LabelCell: UICollectionViewCell {
  10. @IBOutlet weak var iconLabel: UILabel!
  11. }
  12. public enum SwipeDirection {
  13. case left
  14. case right
  15. case none
  16. }
  17. public struct IndicatorInfo {
  18. public var title: String?
  19. public var accessibilityLabel: String?
  20. public init(title: String) {
  21. self.title = title
  22. self.accessibilityLabel = title
  23. }
  24. }
  25. public enum ButtonBarItemSpec<CellType: UICollectionViewCell> {
  26. case nibFile(nibName: String, bundle: Bundle?, width:((IndicatorInfo)-> CGFloat))
  27. public var weight: ((IndicatorInfo) -> CGFloat) {
  28. switch self {
  29. case .nibFile(_, _, let widthCallback):
  30. return widthCallback
  31. }
  32. }
  33. }
  34. public enum PagerScroll {
  35. case no
  36. case yes
  37. case scrollOnlyIfOutOfScreen
  38. }
  39. open class ButtonBarView: UICollectionView {
  40. open lazy var selectedBar: UIView = { [unowned self] in
  41. let bar = UIView(frame: CGRect(x: 0, y: self.frame.size.height - CGFloat(self.selectedBarHeight), width: 0, height: CGFloat(self.selectedBarHeight)))
  42. bar.layer.zPosition = 9999
  43. return bar
  44. }()
  45. internal var selectedBarHeight: CGFloat = 4 {
  46. didSet {
  47. updateSelectedBarYPosition()
  48. }
  49. }
  50. var selectedIndex = 0
  51. required public init?(coder aDecoder: NSCoder) {
  52. super.init(coder: aDecoder)
  53. addSubview(selectedBar)
  54. }
  55. public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
  56. super.init(frame: frame, collectionViewLayout: layout)
  57. addSubview(selectedBar)
  58. }
  59. open func moveTo(index: Int, animated: Bool, swipeDirection: SwipeDirection, pagerScroll: PagerScroll) {
  60. selectedIndex = index
  61. updateSelectedBarPosition(animated, swipeDirection: swipeDirection, pagerScroll: pagerScroll)
  62. }
  63. open func move(fromIndex: Int, toIndex: Int, progressPercentage: CGFloat, pagerScroll: PagerScroll) {
  64. selectedIndex = progressPercentage > 0.5 ? toIndex : fromIndex
  65. let fromFrame = layoutAttributesForItem(at: IndexPath(item: fromIndex, section: 0))!.frame
  66. let numberOfItems = dataSource!.collectionView(self, numberOfItemsInSection: 0)
  67. var toFrame: CGRect
  68. if toIndex < 0 || toIndex > numberOfItems - 1 {
  69. if toIndex < 0 {
  70. let cellAtts = layoutAttributesForItem(at: IndexPath(item: 0, section: 0))
  71. toFrame = cellAtts!.frame.offsetBy(dx: -cellAtts!.frame.size.width, dy: 0)
  72. } else {
  73. let cellAtts = layoutAttributesForItem(at: IndexPath(item: (numberOfItems - 1), section: 0))
  74. toFrame = cellAtts!.frame.offsetBy(dx: cellAtts!.frame.size.width, dy: 0)
  75. }
  76. } else {
  77. toFrame = layoutAttributesForItem(at: IndexPath(item: toIndex, section: 0))!.frame
  78. }
  79. var targetFrame = fromFrame
  80. targetFrame.size.height = selectedBar.frame.size.height
  81. targetFrame.size.width += (toFrame.size.width - fromFrame.size.width) * progressPercentage
  82. targetFrame.origin.x += (toFrame.origin.x - fromFrame.origin.x) * progressPercentage
  83. selectedBar.frame = CGRect(x: targetFrame.origin.x, y: selectedBar.frame.origin.y, width: targetFrame.size.width, height: selectedBar.frame.size.height)
  84. var targetContentOffset: CGFloat = 0.0
  85. if contentSize.width > frame.size.width {
  86. let toContentOffset = contentOffsetForCell(withFrame: toFrame, andIndex: toIndex)
  87. let fromContentOffset = contentOffsetForCell(withFrame: fromFrame, andIndex: fromIndex)
  88. targetContentOffset = fromContentOffset + ((toContentOffset - fromContentOffset) * progressPercentage)
  89. }
  90. setContentOffset(CGPoint(x: targetContentOffset, y: 0), animated: false)
  91. }
  92. open func updateSelectedBarPosition(_ animated: Bool, swipeDirection: SwipeDirection, pagerScroll: PagerScroll) {
  93. var selectedBarFrame = selectedBar.frame
  94. let selectedCellIndexPath = IndexPath(item: selectedIndex, section: 0)
  95. let attributes = layoutAttributesForItem(at: selectedCellIndexPath)
  96. let selectedCellFrame = attributes!.frame
  97. updateContentOffset(animated: animated, pagerScroll: pagerScroll, toFrame: selectedCellFrame, toIndex: (selectedCellIndexPath as NSIndexPath).row)
  98. selectedBarFrame.size.width = selectedCellFrame.size.width
  99. selectedBarFrame.origin.x = selectedCellFrame.origin.x
  100. if animated {
  101. UIView.animate(withDuration: 0.3, animations: { [weak self] in
  102. self?.selectedBar.frame = selectedBarFrame
  103. })
  104. } else {
  105. selectedBar.frame = selectedBarFrame
  106. }
  107. }
  108. // MARK: - Helpers
  109. private func updateContentOffset(animated: Bool, pagerScroll: PagerScroll, toFrame: CGRect, toIndex: Int) {
  110. guard pagerScroll != .no || (pagerScroll != .scrollOnlyIfOutOfScreen && (toFrame.origin.x < contentOffset.x || toFrame.origin.x >= (contentOffset.x + frame.size.width - contentInset.left))) else { return }
  111. let targetContentOffset = contentSize.width > frame.size.width ? contentOffsetForCell(withFrame: toFrame, andIndex: toIndex) : 0
  112. setContentOffset(CGPoint(x: targetContentOffset, y: 0), animated: animated)
  113. }
  114. private func contentOffsetForCell(withFrame cellFrame: CGRect, andIndex index: Int) -> CGFloat {
  115. let alignmentOffset = (frame.size.width - cellFrame.size.width) * 0.5
  116. var contentOffset = cellFrame.origin.x - alignmentOffset
  117. contentOffset = max(0, contentOffset)
  118. contentOffset = min(contentSize.width - frame.size.width, contentOffset)
  119. return contentOffset
  120. }
  121. private func updateSelectedBarYPosition() {
  122. var selectedBarFrame = selectedBar.frame
  123. selectedBarFrame.origin.y = frame.size.height - selectedBarHeight
  124. selectedBarFrame.size.height = selectedBarHeight
  125. selectedBar.frame = selectedBarFrame
  126. }
  127. override open func layoutSubviews() {
  128. super.layoutSubviews()
  129. updateSelectedBarYPosition()
  130. }
  131. }
  132. open class BaseButtonBarPagerTabStripViewController<ButtonBarCellType: UICollectionViewCell>: PagerTabStripViewController, PagerTabStripDataSource, PagerTabStripIsProgressiveDelegate, UICollectionViewDelegate, UICollectionViewDataSource {
  133. public var buttonBarItemSpec: ButtonBarItemSpec<ButtonBarCellType>!
  134. public var changeCurrentIndexProgressive: ((_ oldCell: ButtonBarCellType?, _ newCell: ButtonBarCellType?, _ progressPercentage: CGFloat, _ changeCurrentIndex: Bool, _ animated: Bool) -> Void)?
  135. @IBOutlet public weak var buttonBarView: ButtonBarView!
  136. lazy private var cachedCellWidths: [CGFloat]? = { [unowned self] in
  137. return self.calculateWidths()
  138. }()
  139. public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  140. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  141. delegate = self
  142. datasource = self
  143. }
  144. required public init?(coder aDecoder: NSCoder) {
  145. super.init(coder: aDecoder)
  146. delegate = self
  147. datasource = self
  148. }
  149. open override func viewDidLoad() {
  150. super.viewDidLoad()
  151. let buttonBarViewAux = buttonBarView ?? {
  152. let flowLayout = UICollectionViewFlowLayout()
  153. flowLayout.scrollDirection = .horizontal
  154. let buttonBar = ButtonBarView(frame: .zero, collectionViewLayout: flowLayout)
  155. buttonBar.backgroundColor = .orange
  156. buttonBar.selectedBar.backgroundColor = .black
  157. return buttonBar
  158. }()
  159. buttonBarView = buttonBarViewAux
  160. if buttonBarView.superview == nil {
  161. buttonBarView.translatesAutoresizingMaskIntoConstraints = false
  162. view.addSubview(buttonBarView)
  163. NSLayoutConstraint.activate([
  164. buttonBarView.topAnchor.constraint(equalTo: topLayoutGuide.bottomAnchor),
  165. buttonBarView.rightAnchor.constraint(equalTo: view.rightAnchor),
  166. buttonBarView.leftAnchor.constraint(equalTo: view.leftAnchor),
  167. buttonBarView.heightAnchor.constraint(equalToConstant: 35)
  168. ]
  169. )
  170. }
  171. if buttonBarView.delegate == nil {
  172. buttonBarView.delegate = self
  173. }
  174. if buttonBarView.dataSource == nil {
  175. buttonBarView.dataSource = self
  176. }
  177. buttonBarView.scrollsToTop = false
  178. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  179. flowLayout.scrollDirection = .horizontal
  180. buttonBarView.showsHorizontalScrollIndicator = false
  181. buttonBarView.backgroundColor = .white
  182. buttonBarView.selectedBar.backgroundColor = PresentationTheme.current.colors.orangeUI
  183. buttonBarView.selectedBarHeight = 4.0
  184. // register button bar item cell
  185. switch buttonBarItemSpec! {
  186. case .nibFile(let nibName, let bundle, _):
  187. buttonBarView.register(UINib(nibName: nibName, bundle: bundle), forCellWithReuseIdentifier:"Cell")
  188. }
  189. }
  190. open override func viewWillAppear(_ animated: Bool) {
  191. super.viewWillAppear(animated)
  192. buttonBarView.layoutIfNeeded()
  193. isViewAppearing = true
  194. }
  195. open override func viewDidAppear(_ animated: Bool) {
  196. super.viewDidAppear(animated)
  197. isViewAppearing = false
  198. }
  199. open override func viewDidLayoutSubviews() {
  200. super.viewDidLayoutSubviews()
  201. guard isViewAppearing || isViewRotating else { return }
  202. // Force the UICollectionViewFlowLayout to get laid out again with the new size if
  203. // a) The view is appearing. This ensures that
  204. // collectionView:layout:sizeForItemAtIndexPath: is called for a second time
  205. // when the view is shown and when the view *frame(s)* are actually set
  206. // (we need the view frame's to have been set to work out the size's and on the
  207. // first call to collectionView:layout:sizeForItemAtIndexPath: the view frame(s)
  208. // aren't set correctly)
  209. // b) The view is rotating. This ensures that
  210. // collectionView:layout:sizeForItemAtIndexPath: is called again and can use the views
  211. // *new* frame so that the buttonBarView cell's actually get resized correctly
  212. cachedCellWidths = calculateWidths()
  213. buttonBarView.collectionViewLayout.invalidateLayout()
  214. // When the view first appears or is rotated we also need to ensure that the barButtonView's
  215. // selectedBar is resized and its contentOffset/scroll is set correctly (the selected
  216. // tab/cell may end up either skewed or off screen after a rotation otherwise)
  217. buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .scrollOnlyIfOutOfScreen)
  218. buttonBarView.selectItem(at: IndexPath(item: currentIndex, section: 0), animated: false, scrollPosition: [])
  219. }
  220. // MARK: - View Rotation
  221. open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  222. super.viewWillTransition(to: size, with: coordinator)
  223. }
  224. open override func updateContent() {
  225. super.updateContent()
  226. }
  227. // MARK: - Public Methods
  228. open override func reloadPagerTabStripView() {
  229. super.reloadPagerTabStripView()
  230. guard isViewLoaded else { return }
  231. buttonBarView.reloadData()
  232. cachedCellWidths = calculateWidths()
  233. buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .yes)
  234. }
  235. open func calculateStretchedCellWidths(_ minimumCellWidths: [CGFloat], suggestedStretchedCellWidth: CGFloat, previousNumberOfLargeCells: Int) -> CGFloat {
  236. var numberOfLargeCells = 0
  237. var totalWidthOfLargeCells: CGFloat = 0
  238. for minimumCellWidthValue in minimumCellWidths where minimumCellWidthValue > suggestedStretchedCellWidth {
  239. totalWidthOfLargeCells += minimumCellWidthValue
  240. numberOfLargeCells += 1
  241. }
  242. guard numberOfLargeCells > previousNumberOfLargeCells else { return suggestedStretchedCellWidth }
  243. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  244. let collectionViewAvailiableWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
  245. let numberOfCells = minimumCellWidths.count
  246. let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
  247. let numberOfSmallCells = numberOfCells - numberOfLargeCells
  248. let newSuggestedStretchedCellWidth = (collectionViewAvailiableWidth - totalWidthOfLargeCells - cellSpacingTotal) / CGFloat(numberOfSmallCells)
  249. return calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: newSuggestedStretchedCellWidth, previousNumberOfLargeCells: numberOfLargeCells)
  250. }
  251. open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int) {
  252. guard shouldUpdateButtonBarView else { return }
  253. buttonBarView.moveTo(index: toIndex, animated: true, swipeDirection: toIndex < fromIndex ? .right : .left, pagerScroll: .yes)
  254. }
  255. open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
  256. guard shouldUpdateButtonBarView else { return }
  257. buttonBarView.move(fromIndex: fromIndex, toIndex: toIndex, progressPercentage: progressPercentage, pagerScroll: .yes)
  258. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  259. let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)) as? ButtonBarCellType
  260. let newCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
  261. changeCurrentIndexProgressive(oldCell, newCell, progressPercentage, indexWasChanged, true)
  262. }
  263. }
  264. // MARK: - UICollectionViewDelegateFlowLayut
  265. @objc open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
  266. guard let cellWidthValue = cachedCellWidths?[indexPath.row] else {
  267. fatalError("cachedCellWidths for \(indexPath.row) must not be nil")
  268. }
  269. return CGSize(width: cellWidthValue, height: collectionView.frame.size.height)
  270. }
  271. open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  272. guard indexPath.item != currentIndex else { return }
  273. buttonBarView.moveTo(index: indexPath.item, animated: true, swipeDirection: .none, pagerScroll: .yes)
  274. shouldUpdateButtonBarView = false
  275. let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
  276. let newCell = buttonBarView.cellForItem(at: IndexPath(item: indexPath.item, section: 0)) as? ButtonBarCellType
  277. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  278. changeCurrentIndexProgressive(oldCell, newCell, 1, true, true)
  279. }
  280. moveToViewController(at: indexPath.item)
  281. }
  282. // MARK: - UICollectionViewDataSource
  283. open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  284. return viewControllers.count
  285. }
  286. open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  287. guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? ButtonBarCellType else {
  288. fatalError("UICollectionViewCell should be or extend from ButtonBarViewCell")
  289. }
  290. let childController = viewControllers[indexPath.item] as! IndicatorInfoProvider // swiftlint:disable:this force_cast
  291. let indicatorInfo = childController.indicatorInfo(for: self)
  292. configure(cell: cell, for: indicatorInfo)
  293. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  294. changeCurrentIndexProgressive(currentIndex == indexPath.item ? nil : cell, currentIndex == indexPath.item ? cell : nil, 1, true, false)
  295. }
  296. return cell
  297. }
  298. // MARK: - UIScrollViewDelegate
  299. open override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  300. super.scrollViewDidEndScrollingAnimation(scrollView)
  301. guard scrollView == containerView else { return }
  302. shouldUpdateButtonBarView = true
  303. }
  304. open func configure(cell: ButtonBarCellType, for indicatorInfo: IndicatorInfo) {
  305. fatalError("You must override this method to set up ButtonBarView cell accordingly")
  306. }
  307. private func calculateWidths() -> [CGFloat] {
  308. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  309. let numberOfCells = viewControllers.count
  310. var minimumCellWidths = [CGFloat]()
  311. var collectionViewContentWidth: CGFloat = 0
  312. for viewController in viewControllers {
  313. let childController = viewController as! IndicatorInfoProvider // swiftlint:disable:this force_cast
  314. let indicatorInfo = childController.indicatorInfo(for: self)
  315. switch buttonBarItemSpec! {
  316. case .nibFile(_, _, let widthCallback):
  317. let width = widthCallback(indicatorInfo)
  318. minimumCellWidths.append(width)
  319. collectionViewContentWidth += width
  320. }
  321. }
  322. let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
  323. collectionViewContentWidth += cellSpacingTotal
  324. let collectionViewAvailableVisibleWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
  325. if collectionViewAvailableVisibleWidth < collectionViewContentWidth {
  326. return minimumCellWidths
  327. } else {
  328. let stretchedCellWidthIfAllEqual = (collectionViewAvailableVisibleWidth - cellSpacingTotal) / CGFloat(numberOfCells)
  329. let generalMinimumCellWidth = calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: stretchedCellWidthIfAllEqual, previousNumberOfLargeCells: 0)
  330. var stretchedCellWidths = [CGFloat]()
  331. for minimumCellWidthValue in minimumCellWidths {
  332. let cellWidth = (minimumCellWidthValue > generalMinimumCellWidth) ? minimumCellWidthValue : generalMinimumCellWidth
  333. stretchedCellWidths.append(cellWidth)
  334. }
  335. return stretchedCellWidths
  336. }
  337. }
  338. private var shouldUpdateButtonBarView = true
  339. }
  340. // MARK: Protocols
  341. public protocol IndicatorInfoProvider {
  342. func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo
  343. }
  344. public protocol PagerTabStripIsProgressiveDelegate: class {
  345. func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool)
  346. }
  347. public protocol PagerTabStripDataSource: class {
  348. func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController]
  349. }
  350. // MARK: PagerTabStripViewController
  351. open class PagerTabStripViewController: UIViewController, UIScrollViewDelegate {
  352. @IBOutlet weak public var containerView: UIScrollView!
  353. open weak var delegate: PagerTabStripIsProgressiveDelegate?
  354. open weak var datasource: PagerTabStripDataSource?
  355. open private(set) var viewControllers = [UIViewController]()
  356. open private(set) var currentIndex = 0
  357. open private(set) var preCurrentIndex = 0 // used *only* to store the index to which move when the pager becomes visible
  358. open var pageWidth: CGFloat {
  359. return containerView.bounds.width
  360. }
  361. open var scrollPercentage: CGFloat {
  362. if swipeDirection != .right {
  363. let module = fmod(containerView.contentOffset.x, pageWidth)
  364. return module == 0.0 ? 1.0 : module / pageWidth
  365. }
  366. return 1 - fmod(containerView.contentOffset.x >= 0 ? containerView.contentOffset.x : pageWidth + containerView.contentOffset.x, pageWidth) / pageWidth
  367. }
  368. open var swipeDirection: SwipeDirection {
  369. if containerView.contentOffset.x > lastContentOffset {
  370. return .left
  371. } else if containerView.contentOffset.x < lastContentOffset {
  372. return .right
  373. }
  374. return .none
  375. }
  376. override open func viewDidLoad() {
  377. super.viewDidLoad()
  378. let containerViewAux = containerView ?? {
  379. let containerView = UIScrollView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height))
  380. containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
  381. return containerView
  382. }()
  383. containerView = containerViewAux
  384. if containerView.superview == nil {
  385. view.addSubview(containerView)
  386. }
  387. containerView.bounces = true
  388. containerView.alwaysBounceHorizontal = true
  389. containerView.alwaysBounceVertical = false
  390. containerView.scrollsToTop = false
  391. containerView.delegate = self
  392. containerView.showsVerticalScrollIndicator = false
  393. containerView.showsHorizontalScrollIndicator = false
  394. containerView.isPagingEnabled = true
  395. containerView.backgroundColor = PresentationTheme.current.colors.background
  396. reloadViewControllers()
  397. let childController = viewControllers[currentIndex]
  398. addChildViewController(childController)
  399. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  400. containerView.addSubview(childController.view)
  401. childController.didMove(toParentViewController: self)
  402. }
  403. open override func viewWillAppear(_ animated: Bool) {
  404. super.viewWillAppear(animated)
  405. isViewAppearing = true
  406. childViewControllers.forEach { $0.beginAppearanceTransition(true, animated: animated) }
  407. }
  408. override open func viewDidAppear(_ animated: Bool) {
  409. super.viewDidAppear(animated)
  410. lastSize = containerView.bounds.size
  411. updateIfNeeded()
  412. let needToUpdateCurrentChild = preCurrentIndex != currentIndex
  413. if needToUpdateCurrentChild {
  414. moveToViewController(at: preCurrentIndex)
  415. }
  416. isViewAppearing = false
  417. childViewControllers.forEach { $0.endAppearanceTransition() }
  418. }
  419. open override func viewWillDisappear(_ animated: Bool) {
  420. super.viewWillDisappear(animated)
  421. childViewControllers.forEach { $0.beginAppearanceTransition(false, animated: animated) }
  422. }
  423. open override func viewDidDisappear(_ animated: Bool) {
  424. super.viewDidDisappear(animated)
  425. childViewControllers.forEach { $0.endAppearanceTransition() }
  426. }
  427. override open func viewDidLayoutSubviews() {
  428. super.viewDidLayoutSubviews()
  429. updateIfNeeded()
  430. }
  431. open override var shouldAutomaticallyForwardAppearanceMethods: Bool {
  432. return false
  433. }
  434. open func moveToViewController(at index: Int, animated: Bool = true) {
  435. guard isViewLoaded && view.window != nil && currentIndex != index else {
  436. preCurrentIndex = index
  437. return
  438. }
  439. if animated && abs(currentIndex - index) > 1 {
  440. var tmpViewControllers = viewControllers
  441. let currentChildVC = viewControllers[currentIndex]
  442. let fromIndex = currentIndex < index ? index - 1 : index + 1
  443. let fromChildVC = viewControllers[fromIndex]
  444. tmpViewControllers[currentIndex] = fromChildVC
  445. tmpViewControllers[fromIndex] = currentChildVC
  446. pagerTabStripChildViewControllersForScrolling = tmpViewControllers
  447. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: fromIndex), y: 0), animated: false)
  448. (navigationController?.view ?? view).isUserInteractionEnabled = !animated
  449. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: true)
  450. } else {
  451. (navigationController?.view ?? view).isUserInteractionEnabled = !animated
  452. containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: animated)
  453. }
  454. }
  455. open func moveTo(viewController: UIViewController, animated: Bool = true) {
  456. moveToViewController(at: viewControllers.index(of: viewController)!, animated: animated)
  457. }
  458. // MARK: - PagerTabStripDataSource
  459. open func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
  460. assertionFailure("Sub-class must implement the PagerTabStripDataSource viewControllers(for:) method")
  461. return []
  462. }
  463. // MARK: - Helpers
  464. open func updateIfNeeded() {
  465. if isViewLoaded && !lastSize.equalTo(containerView.bounds.size) {
  466. updateContent()
  467. }
  468. }
  469. open func canMoveTo(index: Int) -> Bool {
  470. return currentIndex != index && viewControllers.count > index
  471. }
  472. open func pageOffsetForChild(at index: Int) -> CGFloat {
  473. return CGFloat(index) * containerView.bounds.width
  474. }
  475. open func offsetForChild(at index: Int) -> CGFloat {
  476. return (CGFloat(index) * containerView.bounds.width) + ((containerView.bounds.width - view.bounds.width) * 0.5)
  477. }
  478. public enum PagerTabStripError: Error {
  479. case viewControllerOutOfBounds
  480. }
  481. open func offsetForChild(viewController: UIViewController) throws -> CGFloat {
  482. guard let index = viewControllers.index(of: viewController) else {
  483. throw PagerTabStripError.viewControllerOutOfBounds
  484. }
  485. return offsetForChild(at: index)
  486. }
  487. open func pageFor(contentOffset: CGFloat) -> Int {
  488. let result = virtualPageFor(contentOffset: contentOffset)
  489. return pageFor(virtualPage: result)
  490. }
  491. open func virtualPageFor(contentOffset: CGFloat) -> Int {
  492. return Int((contentOffset + 1.5 * pageWidth) / pageWidth) - 1
  493. }
  494. open func pageFor(virtualPage: Int) -> Int {
  495. if virtualPage < 0 {
  496. return 0
  497. }
  498. if virtualPage > viewControllers.count - 1 {
  499. return viewControllers.count - 1
  500. }
  501. return virtualPage
  502. }
  503. open func updateContent() {
  504. if lastSize.width != containerView.bounds.size.width {
  505. lastSize = containerView.bounds.size
  506. containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
  507. }
  508. lastSize = containerView.bounds.size
  509. let pagerViewControllers = pagerTabStripChildViewControllersForScrolling ?? viewControllers
  510. containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(pagerViewControllers.count), height: containerView.contentSize.height)
  511. for (index, childController) in pagerViewControllers.enumerated() {
  512. let pageOffsetForChild = self.pageOffsetForChild(at: index)
  513. if fabs(containerView.contentOffset.x - pageOffsetForChild) < containerView.bounds.width {
  514. if childController.parent != nil {
  515. childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
  516. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  517. } else {
  518. childController.beginAppearanceTransition(true, animated: false)
  519. addChildViewController(childController)
  520. childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
  521. childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
  522. containerView.addSubview(childController.view)
  523. childController.didMove(toParentViewController: self)
  524. childController.endAppearanceTransition()
  525. }
  526. } else {
  527. if childController.parent != nil {
  528. childController.beginAppearanceTransition(false, animated: false)
  529. childController.willMove(toParentViewController: nil)
  530. childController.view.removeFromSuperview()
  531. childController.removeFromParentViewController()
  532. childController.endAppearanceTransition()
  533. }
  534. }
  535. }
  536. let oldCurrentIndex = currentIndex
  537. let virtualPage = virtualPageFor(contentOffset: containerView.contentOffset.x)
  538. let newCurrentIndex = pageFor(virtualPage: virtualPage)
  539. currentIndex = newCurrentIndex
  540. preCurrentIndex = currentIndex
  541. let changeCurrentIndex = newCurrentIndex != oldCurrentIndex
  542. if let progressiveDelegate = self as? PagerTabStripIsProgressiveDelegate {
  543. let (fromIndex, toIndex, scrollPercentage) = progressiveIndicatorData(virtualPage)
  544. progressiveDelegate.updateIndicator(for: self, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: scrollPercentage, indexWasChanged: changeCurrentIndex)
  545. }
  546. }
  547. open func reloadPagerTabStripView() {
  548. guard isViewLoaded else { return }
  549. for childController in viewControllers where childController.parent != nil {
  550. childController.beginAppearanceTransition(false, animated: false)
  551. childController.willMove(toParentViewController: nil)
  552. childController.view.removeFromSuperview()
  553. childController.removeFromParentViewController()
  554. childController.endAppearanceTransition()
  555. }
  556. reloadViewControllers()
  557. containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(viewControllers.count), height: containerView.contentSize.height)
  558. if currentIndex >= viewControllers.count {
  559. currentIndex = viewControllers.count - 1
  560. }
  561. preCurrentIndex = currentIndex
  562. containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
  563. updateContent()
  564. }
  565. // MARK: - UIScrollViewDelegate
  566. open func scrollViewDidScroll(_ scrollView: UIScrollView) {
  567. if containerView == scrollView {
  568. updateContent()
  569. lastContentOffset = scrollView.contentOffset.x
  570. }
  571. }
  572. open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
  573. if containerView == scrollView {
  574. lastPageNumber = pageFor(contentOffset: scrollView.contentOffset.x)
  575. }
  576. }
  577. open func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  578. if containerView == scrollView {
  579. pagerTabStripChildViewControllersForScrolling = nil
  580. (navigationController?.view ?? view).isUserInteractionEnabled = true
  581. updateContent()
  582. }
  583. }
  584. // MARK: - Orientation
  585. open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  586. super.viewWillTransition(to: size, with: coordinator)
  587. isViewRotating = true
  588. pageBeforeRotate = currentIndex
  589. coordinator.animate(alongsideTransition: nil) { [weak self] _ in
  590. guard let me = self else { return }
  591. me.isViewRotating = false
  592. me.currentIndex = me.pageBeforeRotate
  593. me.preCurrentIndex = me.currentIndex
  594. me.updateIfNeeded()
  595. }
  596. }
  597. // MARK: Private
  598. private func progressiveIndicatorData(_ virtualPage: Int) -> (Int, Int, CGFloat) {
  599. let count = viewControllers.count
  600. var fromIndex = currentIndex
  601. var toIndex = currentIndex
  602. let direction = swipeDirection
  603. if direction == .left {
  604. if virtualPage > count - 1 {
  605. fromIndex = count - 1
  606. toIndex = count
  607. } else {
  608. if self.scrollPercentage >= 0.5 {
  609. fromIndex = max(toIndex - 1, 0)
  610. } else {
  611. toIndex = fromIndex + 1
  612. }
  613. }
  614. } else if direction == .right {
  615. if virtualPage < 0 {
  616. fromIndex = 0
  617. toIndex = -1
  618. } else {
  619. if self.scrollPercentage > 0.5 {
  620. fromIndex = min(toIndex + 1, count - 1)
  621. } else {
  622. toIndex = fromIndex - 1
  623. }
  624. }
  625. }
  626. return (fromIndex, toIndex, self.scrollPercentage)
  627. }
  628. private func reloadViewControllers() {
  629. guard let dataSource = datasource else {
  630. fatalError("dataSource must not be nil")
  631. }
  632. viewControllers = dataSource.viewControllers(for: self)
  633. // viewControllers
  634. guard !viewControllers.isEmpty else {
  635. fatalError("viewControllers(for:) should provide at least one child view controller")
  636. }
  637. viewControllers.forEach { if !($0 is IndicatorInfoProvider) { fatalError("Every view controller provided by PagerTabStripDataSource's viewControllers(for:) method must conform to IndicatorInfoProvider") }}
  638. }
  639. private var pagerTabStripChildViewControllersForScrolling: [UIViewController]?
  640. private var lastPageNumber = 0
  641. private var lastContentOffset: CGFloat = 0.0
  642. private var pageBeforeRotate = 0
  643. private var lastSize = CGSize(width: 0, height: 0)
  644. internal var isViewRotating = false
  645. internal var isViewAppearing = false
  646. }