BaseButtonBarPagerTabStripViewController.swift 39 KB

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