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

415 lines
20 KiB

6 years ago
  1. // ButtonBarPagerTabStripViewController.swift
  2. // XLPagerTabStrip ( https://github.com/xmartlabs/XLPagerTabStrip )
  3. //
  4. // Copyright (c) 2017 Xmartlabs ( http://xmartlabs.com )
  5. //
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. import Foundation
  25. public enum ButtonBarItemSpec<CellType: UICollectionViewCell> {
  26. case nibFile(nibName: String, bundle: Bundle?, width:((IndicatorInfo)-> CGFloat))
  27. case cellClass(width:((IndicatorInfo)-> CGFloat))
  28. public var weight: ((IndicatorInfo) -> CGFloat) {
  29. switch self {
  30. case .cellClass(let widthCallback):
  31. return widthCallback
  32. case .nibFile(_, _, let widthCallback):
  33. return widthCallback
  34. }
  35. }
  36. }
  37. public struct ButtonBarPagerTabStripSettings {
  38. public struct Style {
  39. public var buttonBarBackgroundColor: UIColor?
  40. public var buttonBarMinimumInteritemSpacing: CGFloat?
  41. public var buttonBarMinimumLineSpacing: CGFloat?
  42. public var buttonBarLeftContentInset: CGFloat?
  43. public var buttonBarRightContentInset: CGFloat?
  44. public var selectedBarBackgroundColor = UIColor.black
  45. public var selectedBarHeight: CGFloat = 5
  46. public var selectedBarVerticalAlignment: SelectedBarVerticalAlignment = .bottom
  47. public var buttonBarItemBackgroundColor: UIColor?
  48. public var buttonBarItemFont = UIFont.systemFont(ofSize: 18)
  49. public var buttonBarItemLeftRightMargin: CGFloat = 8
  50. public var buttonBarItemTitleColor: UIColor?
  51. @available(*, deprecated: 7.0.0) public var buttonBarItemsShouldFillAvailiableWidth: Bool {
  52. set {
  53. buttonBarItemsShouldFillAvailableWidth = newValue
  54. }
  55. get {
  56. return buttonBarItemsShouldFillAvailableWidth
  57. }
  58. }
  59. public var buttonBarItemsShouldFillAvailableWidth = true
  60. // only used if button bar is created programaticaly and not using storyboards or nib files
  61. public var buttonBarHeight: CGFloat?
  62. }
  63. public var style = Style()
  64. }
  65. open class ButtonBarPagerTabStripViewController: PagerTabStripViewController, PagerTabStripDataSource, PagerTabStripIsProgressiveDelegate, UICollectionViewDelegate, UICollectionViewDataSource {
  66. public var settings = ButtonBarPagerTabStripSettings()
  67. public var buttonBarItemSpec: ButtonBarItemSpec<ButtonBarViewCell>!
  68. public var changeCurrentIndex: ((_ oldCell: ButtonBarViewCell?, _ newCell: ButtonBarViewCell?, _ animated: Bool) -> Void)?
  69. public var changeCurrentIndexProgressive: ((_ oldCell: ButtonBarViewCell?, _ newCell: ButtonBarViewCell?, _ progressPercentage: CGFloat, _ changeCurrentIndex: Bool, _ animated: Bool) -> Void)?
  70. @IBOutlet public weak var buttonBarView: ButtonBarView!
  71. lazy private var cachedCellWidths: [CGFloat]? = { [unowned self] in
  72. return self.calculateWidths()
  73. }()
  74. override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  75. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  76. delegate = self
  77. datasource = self
  78. }
  79. required public init?(coder aDecoder: NSCoder) {
  80. super.init(coder: aDecoder)
  81. delegate = self
  82. datasource = self
  83. }
  84. open override func viewDidLoad() {
  85. super.viewDidLoad()
  86. var bundle = Bundle(for: ButtonBarViewCell.self)
  87. if let resourcePath = bundle.path(forResource: "XLPagerTabStrip", ofType: "bundle") {
  88. if let resourcesBundle = Bundle(path: resourcePath) {
  89. bundle = resourcesBundle
  90. }
  91. }
  92. buttonBarItemSpec = .nibFile(nibName: "ButtonCell", bundle: bundle, width: { [weak self] (childItemInfo) -> CGFloat in
  93. let label = UILabel()
  94. label.translatesAutoresizingMaskIntoConstraints = false
  95. label.font = self?.settings.style.buttonBarItemFont
  96. label.text = childItemInfo.title
  97. let labelSize = label.intrinsicContentSize
  98. return labelSize.width + (self?.settings.style.buttonBarItemLeftRightMargin ?? 8) * 2
  99. })
  100. let buttonBarViewAux = buttonBarView ?? {
  101. let flowLayout = UICollectionViewFlowLayout()
  102. flowLayout.scrollDirection = .horizontal
  103. let buttonBarHeight = settings.style.buttonBarHeight ?? 44
  104. let buttonBar = ButtonBarView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: buttonBarHeight), collectionViewLayout: flowLayout)
  105. buttonBar.backgroundColor = .orange
  106. buttonBar.selectedBar.backgroundColor = .black
  107. buttonBar.autoresizingMask = .flexibleWidth
  108. var newContainerViewFrame = containerView.frame
  109. newContainerViewFrame.origin.y = buttonBarHeight
  110. newContainerViewFrame.size.height = containerView.frame.size.height - (buttonBarHeight - containerView.frame.origin.y)
  111. containerView.frame = newContainerViewFrame
  112. return buttonBar
  113. }()
  114. buttonBarView = buttonBarViewAux
  115. if buttonBarView.superview == nil {
  116. view.addSubview(buttonBarView)
  117. }
  118. if buttonBarView.delegate == nil {
  119. buttonBarView.delegate = self
  120. }
  121. if buttonBarView.dataSource == nil {
  122. buttonBarView.dataSource = self
  123. }
  124. buttonBarView.scrollsToTop = false
  125. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  126. flowLayout.scrollDirection = .horizontal
  127. flowLayout.minimumInteritemSpacing = settings.style.buttonBarMinimumInteritemSpacing ?? flowLayout.minimumInteritemSpacing
  128. flowLayout.minimumLineSpacing = settings.style.buttonBarMinimumLineSpacing ?? flowLayout.minimumLineSpacing
  129. let sectionInset = flowLayout.sectionInset
  130. flowLayout.sectionInset = UIEdgeInsets(top: sectionInset.top, left: settings.style.buttonBarLeftContentInset ?? sectionInset.left, bottom: sectionInset.bottom, right: settings.style.buttonBarRightContentInset ?? sectionInset.right)
  131. buttonBarView.showsHorizontalScrollIndicator = false
  132. buttonBarView.backgroundColor = settings.style.buttonBarBackgroundColor ?? buttonBarView.backgroundColor
  133. buttonBarView.selectedBar.backgroundColor = settings.style.selectedBarBackgroundColor
  134. buttonBarView.selectedBarHeight = settings.style.selectedBarHeight
  135. buttonBarView.selectedBarVerticalAlignment = settings.style.selectedBarVerticalAlignment
  136. // register button bar item cell
  137. switch buttonBarItemSpec! {
  138. case .nibFile(let nibName, let bundle, _):
  139. buttonBarView.register(UINib(nibName: nibName, bundle: bundle), forCellWithReuseIdentifier:"Cell")
  140. case .cellClass:
  141. buttonBarView.register(ButtonBarViewCell.self, forCellWithReuseIdentifier:"Cell")
  142. }
  143. //-
  144. }
  145. open override func viewWillAppear(_ animated: Bool) {
  146. super.viewWillAppear(animated)
  147. buttonBarView.layoutIfNeeded()
  148. }
  149. open override func viewDidLayoutSubviews() {
  150. super.viewDidLayoutSubviews()
  151. guard isViewAppearing || isViewRotating else { return }
  152. // Force the UICollectionViewFlowLayout to get laid out again with the new size if
  153. // a) The view is appearing. This ensures that
  154. // collectionView:layout:sizeForItemAtIndexPath: is called for a second time
  155. // when the view is shown and when the view *frame(s)* are actually set
  156. // (we need the view frame's to have been set to work out the size's and on the
  157. // first call to collectionView:layout:sizeForItemAtIndexPath: the view frame(s)
  158. // aren't set correctly)
  159. // b) The view is rotating. This ensures that
  160. // collectionView:layout:sizeForItemAtIndexPath: is called again and can use the views
  161. // *new* frame so that the buttonBarView cell's actually get resized correctly
  162. cachedCellWidths = calculateWidths()
  163. buttonBarView.collectionViewLayout.invalidateLayout()
  164. // When the view first appears or is rotated we also need to ensure that the barButtonView's
  165. // selectedBar is resized and its contentOffset/scroll is set correctly (the selected
  166. // tab/cell may end up either skewed or off screen after a rotation otherwise)
  167. buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .scrollOnlyIfOutOfScreen)
  168. buttonBarView.selectItem(at: IndexPath(item: currentIndex, section: 0), animated: false, scrollPosition: [])
  169. }
  170. // MARK: - Public Methods
  171. open override func reloadPagerTabStripView() {
  172. super.reloadPagerTabStripView()
  173. guard isViewLoaded else { return }
  174. buttonBarView.reloadData()
  175. cachedCellWidths = calculateWidths()
  176. buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .yes)
  177. }
  178. open func calculateStretchedCellWidths(_ minimumCellWidths: [CGFloat], suggestedStretchedCellWidth: CGFloat, previousNumberOfLargeCells: Int) -> CGFloat {
  179. var numberOfLargeCells = 0
  180. var totalWidthOfLargeCells: CGFloat = 0
  181. for minimumCellWidthValue in minimumCellWidths where minimumCellWidthValue > suggestedStretchedCellWidth {
  182. totalWidthOfLargeCells += minimumCellWidthValue
  183. numberOfLargeCells += 1
  184. }
  185. guard numberOfLargeCells > previousNumberOfLargeCells else { return suggestedStretchedCellWidth }
  186. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  187. let collectionViewAvailiableWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
  188. let numberOfCells = minimumCellWidths.count
  189. let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
  190. let numberOfSmallCells = numberOfCells - numberOfLargeCells
  191. let newSuggestedStretchedCellWidth = (collectionViewAvailiableWidth - totalWidthOfLargeCells - cellSpacingTotal) / CGFloat(numberOfSmallCells)
  192. return calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: newSuggestedStretchedCellWidth, previousNumberOfLargeCells: numberOfLargeCells)
  193. }
  194. open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int) {
  195. guard shouldUpdateButtonBarView else { return }
  196. buttonBarView.moveTo(index: toIndex, animated: false, swipeDirection: toIndex < fromIndex ? .right : .left, pagerScroll: .yes)
  197. if let changeCurrentIndex = changeCurrentIndex {
  198. let oldIndexPath = IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)
  199. let newIndexPath = IndexPath(item: currentIndex, section: 0)
  200. let cells = cellForItems(at: [oldIndexPath, newIndexPath], reloadIfNotVisible: collectionViewDidLoad)
  201. changeCurrentIndex(cells.first!, cells.last!, true)
  202. }
  203. }
  204. open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
  205. guard shouldUpdateButtonBarView else { return }
  206. buttonBarView.move(fromIndex: fromIndex, toIndex: toIndex, progressPercentage: progressPercentage, pagerScroll: .yes)
  207. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  208. let oldIndexPath = IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)
  209. let newIndexPath = IndexPath(item: currentIndex, section: 0)
  210. let cells = cellForItems(at: [oldIndexPath, newIndexPath], reloadIfNotVisible: collectionViewDidLoad)
  211. changeCurrentIndexProgressive(cells.first!, cells.last!, progressPercentage, indexWasChanged, true)
  212. }
  213. }
  214. private func cellForItems(at indexPaths: [IndexPath], reloadIfNotVisible reload: Bool = true) -> [ButtonBarViewCell?] {
  215. let cells = indexPaths.map { buttonBarView.cellForItem(at: $0) as? ButtonBarViewCell }
  216. if reload {
  217. let indexPathsToReload = cells.enumerated()
  218. .flatMap { (arg) -> IndexPath? in
  219. let (index, cell) = arg
  220. return cell == nil ? indexPaths[index] : nil
  221. }
  222. .flatMap { (indexPath: IndexPath) -> IndexPath? in
  223. return (indexPath.item >= 0 && indexPath.item < buttonBarView.numberOfItems(inSection: indexPath.section)) ? indexPath : nil
  224. }
  225. if !indexPathsToReload.isEmpty {
  226. buttonBarView.reloadItems(at: indexPathsToReload)
  227. }
  228. }
  229. return cells
  230. }
  231. // MARK: - UICollectionViewDelegateFlowLayut
  232. @objc open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
  233. guard let cellWidthValue = cachedCellWidths?[indexPath.row] else {
  234. fatalError("cachedCellWidths for \(indexPath.row) must not be nil")
  235. }
  236. return CGSize(width: cellWidthValue, height: collectionView.frame.size.height)
  237. }
  238. open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  239. guard indexPath.item != currentIndex else { return }
  240. buttonBarView.moveTo(index: indexPath.item, animated: true, swipeDirection: .none, pagerScroll: .yes)
  241. shouldUpdateButtonBarView = false
  242. let oldIndexPath = IndexPath(item: currentIndex, section: 0)
  243. let newIndexPath = IndexPath(item: indexPath.item, section: 0)
  244. let cells = cellForItems(at: [oldIndexPath, newIndexPath], reloadIfNotVisible: collectionViewDidLoad)
  245. if pagerBehaviour.isProgressiveIndicator {
  246. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  247. changeCurrentIndexProgressive(cells.first!, cells.last!, 1, true, true)
  248. }
  249. } else {
  250. if let changeCurrentIndex = changeCurrentIndex {
  251. changeCurrentIndex(cells.first!, cells.last!, true)
  252. }
  253. }
  254. moveToViewController(at: indexPath.item)
  255. }
  256. // MARK: - UICollectionViewDataSource
  257. open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  258. return viewControllers.count
  259. }
  260. open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  261. guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? ButtonBarViewCell else {
  262. fatalError("UICollectionViewCell should be or extend from ButtonBarViewCell")
  263. }
  264. collectionViewDidLoad = true
  265. let childController = viewControllers[indexPath.item] as! IndicatorInfoProvider // swiftlint:disable:this force_cast
  266. let indicatorInfo = childController.indicatorInfo(for: self)
  267. cell.label.text = indicatorInfo.title
  268. cell.accessibilityLabel = indicatorInfo.accessibilityLabel
  269. cell.label.font = settings.style.buttonBarItemFont
  270. cell.label.textColor = settings.style.buttonBarItemTitleColor ?? cell.label.textColor
  271. cell.contentView.backgroundColor = settings.style.buttonBarItemBackgroundColor ?? cell.contentView.backgroundColor
  272. cell.backgroundColor = settings.style.buttonBarItemBackgroundColor ?? cell.backgroundColor
  273. if let image = indicatorInfo.image {
  274. cell.imageView.image = image
  275. }
  276. if let highlightedImage = indicatorInfo.highlightedImage {
  277. cell.imageView.highlightedImage = highlightedImage
  278. }
  279. configureCell(cell, indicatorInfo: indicatorInfo)
  280. if pagerBehaviour.isProgressiveIndicator {
  281. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  282. changeCurrentIndexProgressive(currentIndex == indexPath.item ? nil : cell, currentIndex == indexPath.item ? cell : nil, 1, true, false)
  283. }
  284. } else {
  285. if let changeCurrentIndex = changeCurrentIndex {
  286. changeCurrentIndex(currentIndex == indexPath.item ? nil : cell, currentIndex == indexPath.item ? cell : nil, false)
  287. }
  288. }
  289. cell.isAccessibilityElement = true
  290. cell.accessibilityLabel = cell.label.text
  291. cell.accessibilityTraits |= UIAccessibilityTraitButton
  292. cell.accessibilityTraits |= UIAccessibilityTraitHeader
  293. return cell
  294. }
  295. // MARK: - UIScrollViewDelegate
  296. open override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  297. super.scrollViewDidEndScrollingAnimation(scrollView)
  298. guard scrollView == containerView else { return }
  299. shouldUpdateButtonBarView = true
  300. }
  301. open func configureCell(_ cell: ButtonBarViewCell, indicatorInfo: IndicatorInfo) {
  302. }
  303. private func calculateWidths() -> [CGFloat] {
  304. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  305. let numberOfCells = viewControllers.count
  306. var minimumCellWidths = [CGFloat]()
  307. var collectionViewContentWidth: CGFloat = 0
  308. for viewController in viewControllers {
  309. let childController = viewController as! IndicatorInfoProvider // swiftlint:disable:this force_cast
  310. let indicatorInfo = childController.indicatorInfo(for: self)
  311. switch buttonBarItemSpec! {
  312. case .cellClass(let widthCallback):
  313. let width = widthCallback(indicatorInfo)
  314. minimumCellWidths.append(width)
  315. collectionViewContentWidth += width
  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 !settings.style.buttonBarItemsShouldFillAvailableWidth || 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. private var collectionViewDidLoad = false
  340. }