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.

352 lines
18 KiB

6 years ago
6 years ago
  1. // BaseButtonBarPagerTabStripViewController.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. open class BaseButtonBarPagerTabStripViewController<ButtonBarCellType: UICollectionViewCell>: PagerTabStripViewController, PagerTabStripDataSource, PagerTabStripIsProgressiveDelegate, UICollectionViewDelegate, UICollectionViewDataSource {
  26. public var settings = ButtonBarPagerTabStripSettings()
  27. public var buttonBarItemSpec: ButtonBarItemSpec<ButtonBarCellType>!
  28. public var changeCurrentIndex: ((_ oldCell: ButtonBarCellType?, _ newCell: ButtonBarCellType?, _ animated: Bool) -> Void)?
  29. public var changeCurrentIndexProgressive: ((_ oldCell: ButtonBarCellType?, _ newCell: ButtonBarCellType?, _ progressPercentage: CGFloat, _ changeCurrentIndex: Bool, _ animated: Bool) -> Void)?
  30. @IBOutlet public weak var buttonBarView: ButtonBarView!
  31. lazy private var cachedCellWidths: [CGFloat]? = { [unowned self] in
  32. return self.calculateWidths()
  33. }()
  34. public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  35. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  36. delegate = self
  37. datasource = self
  38. }
  39. required public init?(coder aDecoder: NSCoder) {
  40. super.init(coder: aDecoder)
  41. delegate = self
  42. datasource = self
  43. }
  44. open override func viewDidLoad() {
  45. super.viewDidLoad()
  46. let buttonBarViewAux = buttonBarView ?? {
  47. let flowLayout = UICollectionViewFlowLayout()
  48. flowLayout.scrollDirection = .horizontal
  49. flowLayout.sectionInset = UIEdgeInsets(top: 0, left: settings.style.buttonBarLeftContentInset ?? 35, bottom: 0, right: settings.style.buttonBarRightContentInset ?? 35)
  50. let buttonBarHeight = settings.style.buttonBarHeight ?? 44
  51. let buttonBar = ButtonBarView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: buttonBarHeight), collectionViewLayout: flowLayout)
  52. buttonBar.backgroundColor = .orange
  53. buttonBar.selectedBar.backgroundColor = .black
  54. buttonBar.autoresizingMask = .flexibleWidth
  55. var newContainerViewFrame = containerView.frame
  56. newContainerViewFrame.origin.y = buttonBarHeight
  57. newContainerViewFrame.size.height = containerView.frame.size.height - (buttonBarHeight - containerView.frame.origin.y)
  58. containerView.frame = newContainerViewFrame
  59. return buttonBar
  60. }()
  61. buttonBarView = buttonBarViewAux
  62. if buttonBarView.superview == nil {
  63. view.addSubview(buttonBarView)
  64. }
  65. if buttonBarView.delegate == nil {
  66. buttonBarView.delegate = self
  67. }
  68. if buttonBarView.dataSource == nil {
  69. buttonBarView.dataSource = self
  70. }
  71. buttonBarView.scrollsToTop = false
  72. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  73. flowLayout.scrollDirection = .horizontal
  74. flowLayout.minimumInteritemSpacing = settings.style.buttonBarMinimumInteritemSpacing ?? flowLayout.minimumInteritemSpacing
  75. flowLayout.minimumLineSpacing = settings.style.buttonBarMinimumLineSpacing ?? flowLayout.minimumLineSpacing
  76. let sectionInset = flowLayout.sectionInset
  77. flowLayout.sectionInset = UIEdgeInsets(top: sectionInset.top, left: settings.style.buttonBarLeftContentInset ?? sectionInset.left, bottom: sectionInset.bottom, right: settings.style.buttonBarRightContentInset ?? sectionInset.right)
  78. buttonBarView.showsHorizontalScrollIndicator = false
  79. buttonBarView.backgroundColor = settings.style.buttonBarBackgroundColor ?? buttonBarView.backgroundColor
  80. buttonBarView.selectedBar.backgroundColor = settings.style.selectedBarBackgroundColor
  81. buttonBarView.selectedBarVerticalAlignment = settings.style.selectedBarVerticalAlignment
  82. buttonBarView.selectedBarHeight = settings.style.selectedBarHeight
  83. // register button bar item cell
  84. switch buttonBarItemSpec! {
  85. case .nibFile(let nibName, let bundle, _):
  86. buttonBarView.register(UINib(nibName: nibName, bundle: bundle), forCellWithReuseIdentifier:"Cell")
  87. case .cellClass:
  88. buttonBarView.register(ButtonBarCellType.self, forCellWithReuseIdentifier:"Cell")
  89. }
  90. //-
  91. }
  92. open override func viewWillAppear(_ animated: Bool) {
  93. super.viewWillAppear(animated)
  94. buttonBarView.layoutIfNeeded()
  95. isViewAppearing = true
  96. }
  97. open override func viewDidAppear(_ animated: Bool) {
  98. super.viewDidAppear(animated)
  99. isViewAppearing = false
  100. }
  101. open override func viewDidLayoutSubviews() {
  102. super.viewDidLayoutSubviews()
  103. guard isViewAppearing || isViewRotating else { return }
  104. // Force the UICollectionViewFlowLayout to get laid out again with the new size if
  105. // a) The view is appearing. This ensures that
  106. // collectionView:layout:sizeForItemAtIndexPath: is called for a second time
  107. // when the view is shown and when the view *frame(s)* are actually set
  108. // (we need the view frame's to have been set to work out the size's and on the
  109. // first call to collectionView:layout:sizeForItemAtIndexPath: the view frame(s)
  110. // aren't set correctly)
  111. // b) The view is rotating. This ensures that
  112. // collectionView:layout:sizeForItemAtIndexPath: is called again and can use the views
  113. // *new* frame so that the buttonBarView cell's actually get resized correctly
  114. cachedCellWidths = calculateWidths()
  115. buttonBarView.collectionViewLayout.invalidateLayout()
  116. // When the view first appears or is rotated we also need to ensure that the barButtonView's
  117. // selectedBar is resized and its contentOffset/scroll is set correctly (the selected
  118. // tab/cell may end up either skewed or off screen after a rotation otherwise)
  119. buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .scrollOnlyIfOutOfScreen)
  120. buttonBarView.selectItem(at: IndexPath(item: currentIndex, section: 0), animated: false, scrollPosition: [])
  121. }
  122. // MARK: - View Rotation
  123. open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
  124. super.viewWillTransition(to: size, with: coordinator)
  125. }
  126. // MARK: - Public Methods
  127. open override func reloadPagerTabStripView() {
  128. super.reloadPagerTabStripView()
  129. guard isViewLoaded else { return }
  130. buttonBarView.reloadData()
  131. cachedCellWidths = calculateWidths()
  132. buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .yes)
  133. }
  134. open func calculateStretchedCellWidths(_ minimumCellWidths: [CGFloat], suggestedStretchedCellWidth: CGFloat, previousNumberOfLargeCells: Int) -> CGFloat {
  135. var numberOfLargeCells = 0
  136. var totalWidthOfLargeCells: CGFloat = 0
  137. for minimumCellWidthValue in minimumCellWidths where minimumCellWidthValue > suggestedStretchedCellWidth {
  138. totalWidthOfLargeCells += minimumCellWidthValue
  139. numberOfLargeCells += 1
  140. }
  141. guard numberOfLargeCells > previousNumberOfLargeCells else { return suggestedStretchedCellWidth }
  142. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  143. let collectionViewAvailiableWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
  144. let numberOfCells = minimumCellWidths.count
  145. let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
  146. let numberOfSmallCells = numberOfCells - numberOfLargeCells
  147. let newSuggestedStretchedCellWidth = (collectionViewAvailiableWidth - totalWidthOfLargeCells - cellSpacingTotal) / CGFloat(numberOfSmallCells)
  148. return calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: newSuggestedStretchedCellWidth, previousNumberOfLargeCells: numberOfLargeCells)
  149. }
  150. open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int) {
  151. guard shouldUpdateButtonBarView else { return }
  152. buttonBarView.moveTo(index: toIndex, animated: true, swipeDirection: toIndex < fromIndex ? .right : .left, pagerScroll: .yes)
  153. if let changeCurrentIndex = changeCurrentIndex {
  154. let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)) as? ButtonBarCellType
  155. let newCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
  156. changeCurrentIndex(oldCell, newCell, true)
  157. }
  158. }
  159. open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
  160. guard shouldUpdateButtonBarView else { return }
  161. buttonBarView.move(fromIndex: fromIndex, toIndex: toIndex, progressPercentage: progressPercentage, pagerScroll: .yes)
  162. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  163. let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)) as? ButtonBarCellType
  164. let newCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
  165. changeCurrentIndexProgressive(oldCell, newCell, progressPercentage, indexWasChanged, true)
  166. }
  167. }
  168. // MARK: - UICollectionViewDelegateFlowLayut
  169. @objc open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
  170. guard let cellWidthValue = cachedCellWidths?[indexPath.row] else {
  171. fatalError("cachedCellWidths for \(indexPath.row) must not be nil")
  172. }
  173. return CGSize(width: cellWidthValue, height: collectionView.frame.size.height)
  174. }
  175. open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
  176. guard indexPath.item != currentIndex else { return }
  177. buttonBarView.moveTo(index: indexPath.item, animated: true, swipeDirection: .none, pagerScroll: .yes)
  178. shouldUpdateButtonBarView = false
  179. let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
  180. let newCell = buttonBarView.cellForItem(at: IndexPath(item: indexPath.item, section: 0)) as? ButtonBarCellType
  181. if pagerBehaviour.isProgressiveIndicator {
  182. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  183. changeCurrentIndexProgressive(oldCell, newCell, 1, true, true)
  184. }
  185. } else {
  186. if let changeCurrentIndex = changeCurrentIndex {
  187. changeCurrentIndex(oldCell, newCell, true)
  188. }
  189. }
  190. moveToViewController(at: indexPath.item)
  191. }
  192. // MARK: - UICollectionViewDataSource
  193. open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
  194. return viewControllers.count
  195. }
  196. open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  197. guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? ButtonBarCellType else {
  198. fatalError("UICollectionViewCell should be or extend from ButtonBarViewCell")
  199. }
  200. let childController = viewControllers[indexPath.item] as! IndicatorInfoProvider // swiftlint:disable:this force_cast
  201. let indicatorInfo = childController.indicatorInfo(for: self)
  202. configure(cell: cell, for: indicatorInfo)
  203. if pagerBehaviour.isProgressiveIndicator {
  204. if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
  205. changeCurrentIndexProgressive(currentIndex == indexPath.item ? nil : cell, currentIndex == indexPath.item ? cell : nil, 1, true, false)
  206. }
  207. } else {
  208. if let changeCurrentIndex = changeCurrentIndex {
  209. changeCurrentIndex(currentIndex == indexPath.item ? nil : cell, currentIndex == indexPath.item ? cell : nil, false)
  210. }
  211. }
  212. return cell
  213. }
  214. // MARK: - UIScrollViewDelegate
  215. open override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
  216. super.scrollViewDidEndScrollingAnimation(scrollView)
  217. guard scrollView == containerView else { return }
  218. shouldUpdateButtonBarView = true
  219. }
  220. open func configure(cell: ButtonBarCellType, for indicatorInfo: IndicatorInfo) {
  221. fatalError("You must override this method to set up ButtonBarView cell accordingly")
  222. }
  223. private func calculateWidths() -> [CGFloat] {
  224. let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
  225. let numberOfCells = viewControllers.count
  226. var minimumCellWidths = [CGFloat]()
  227. var collectionViewContentWidth: CGFloat = 0
  228. for viewController in viewControllers {
  229. let childController = viewController as! IndicatorInfoProvider // swiftlint:disable:this force_cast
  230. let indicatorInfo = childController.indicatorInfo(for: self)
  231. switch buttonBarItemSpec! {
  232. case .cellClass(let widthCallback):
  233. let width = widthCallback(indicatorInfo)
  234. minimumCellWidths.append(width)
  235. collectionViewContentWidth += width
  236. case .nibFile(_, _, let widthCallback):
  237. let width = widthCallback(indicatorInfo)
  238. minimumCellWidths.append(width)
  239. collectionViewContentWidth += width
  240. }
  241. }
  242. let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
  243. collectionViewContentWidth += cellSpacingTotal
  244. let collectionViewAvailableVisibleWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
  245. if !settings.style.buttonBarItemsShouldFillAvailableWidth || collectionViewAvailableVisibleWidth < collectionViewContentWidth {
  246. return minimumCellWidths
  247. } else {
  248. let stretchedCellWidthIfAllEqual = (collectionViewAvailableVisibleWidth - cellSpacingTotal) / CGFloat(numberOfCells)
  249. let generalMinimumCellWidth = calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: stretchedCellWidthIfAllEqual, previousNumberOfLargeCells: 0)
  250. var stretchedCellWidths = [CGFloat]()
  251. for minimumCellWidthValue in minimumCellWidths {
  252. let cellWidth = (minimumCellWidthValue > generalMinimumCellWidth) ? minimumCellWidthValue : generalMinimumCellWidth
  253. stretchedCellWidths.append(cellWidth)
  254. }
  255. return stretchedCellWidths
  256. }
  257. }
  258. private var shouldUpdateButtonBarView = true
  259. }
  260. open class ExampleBaseButtonBarPagerTabStripViewController: BaseButtonBarPagerTabStripViewController<ButtonBarViewCell> {
  261. public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
  262. super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
  263. initialize()
  264. }
  265. public required init?(coder aDecoder: NSCoder) {
  266. super.init(coder: aDecoder)
  267. initialize()
  268. }
  269. open func initialize() {
  270. var bundle = Bundle(for: ButtonBarViewCell.self)
  271. if let resourcePath = bundle.path(forResource: "XLPagerTabStrip", ofType: "bundle") {
  272. if let resourcesBundle = Bundle(path: resourcePath) {
  273. bundle = resourcesBundle
  274. }
  275. }
  276. buttonBarItemSpec = .nibFile(nibName: "ButtonCell", bundle: bundle, width: { [weak self] (childItemInfo) -> CGFloat in
  277. let label = UILabel()
  278. label.translatesAutoresizingMaskIntoConstraints = false
  279. label.font = self?.settings.style.buttonBarItemFont ?? label.font
  280. label.text = childItemInfo.title
  281. let labelSize = label.intrinsicContentSize
  282. return labelSize.width + CGFloat(self?.settings.style.buttonBarItemLeftRightMargin ?? 8 * 2)
  283. })
  284. }
  285. open override func configure(cell: ButtonBarViewCell, for indicatorInfo: IndicatorInfo) {
  286. cell.label.text = indicatorInfo.title
  287. cell.accessibilityLabel = indicatorInfo.accessibilityLabel
  288. if let image = indicatorInfo.image {
  289. cell.imageView.image = image
  290. }
  291. if let highlightedImage = indicatorInfo.highlightedImage {
  292. cell.imageView.highlightedImage = highlightedImage
  293. }
  294. }
  295. }