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