123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904 |
- //
- // BaseButtonBarPagerTabStripViewController.swift
- // VLC-iOS
- //
- // Created by Carola Nitz on 5/3/18.
- // Copyright © 2018 VideoLAN. All rights reserved.
- //
- import Foundation
- class IconLabelCell: UICollectionViewCell {
- @IBOutlet weak var iconImage: UIImageView!
- @IBOutlet weak var iconLabel: UILabel!
- }
- public enum SwipeDirection {
- case left
- case right
- case none
- }
- public struct ButtonBarPagerTabStripSettings {
- public struct Style {
- public var buttonBarBackgroundColor: UIColor?
- public var buttonBarMinimumInteritemSpacing: CGFloat?
- public var buttonBarMinimumLineSpacing: CGFloat?
- public var selectedBarBackgroundColor = UIColor.black
- public var selectedBarHeight: CGFloat = 5
- public var selectedBarVerticalAlignment: SelectedBarVerticalAlignment = .bottom
- public var buttonBarItemFont = UIFont.systemFont(ofSize: 18)
- public var buttonBarItemLeftRightMargin: CGFloat = 8
- public var buttonBarItemTitleColor: UIColor?
- public var buttonBarItemsShouldFillAvailableWidth = true
- // only used if button bar is created programaticaly and not using storyboards or nib files
- public var buttonBarHeight: CGFloat?
- }
- public var style = Style()
- }
- public struct IndicatorInfo {
- public var title: String?
- public var image: UIImage?
- public var accessibilityLabel: String?
- public init(title: String?, image: UIImage?) {
- self.title = title
- self.accessibilityLabel = title
- self.image = image
- }
- }
- public enum ButtonBarItemSpec<CellType: UICollectionViewCell> {
- case nibFile(nibName: String, bundle: Bundle?, width:((IndicatorInfo)-> CGFloat))
- case cellClass(width:((IndicatorInfo)-> CGFloat))
- public var weight: ((IndicatorInfo) -> CGFloat) {
- switch self {
- case .cellClass(let widthCallback):
- return widthCallback
- case .nibFile(_, _, let widthCallback):
- return widthCallback
- }
- }
- }
- public enum PagerScroll {
- case no
- case yes
- case scrollOnlyIfOutOfScreen
- }
- public enum SelectedBarAlignment {
- case left
- case center
- case right
- case progressive
- }
- public enum SelectedBarVerticalAlignment {
- case top
- case middle
- case bottom
- }
- open class ButtonBarView: UICollectionView {
- open lazy var selectedBar: UIView = { [unowned self] in
- let bar = UIView(frame: CGRect(x: 0, y: self.frame.size.height - CGFloat(self.selectedBarHeight), width: 0, height: CGFloat(self.selectedBarHeight)))
- bar.layer.zPosition = 9999
- return bar
- }()
- internal var selectedBarHeight: CGFloat = 4 {
- didSet {
- updateSelectedBarYPosition()
- }
- }
- var selectedBarVerticalAlignment: SelectedBarVerticalAlignment = .bottom
- var selectedBarAlignment: SelectedBarAlignment = .center
- var selectedIndex = 0
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- addSubview(selectedBar)
- }
- public override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
- super.init(frame: frame, collectionViewLayout: layout)
- addSubview(selectedBar)
- }
- open func moveTo(index: Int, animated: Bool, swipeDirection: SwipeDirection, pagerScroll: PagerScroll) {
- selectedIndex = index
- updateSelectedBarPosition(animated, swipeDirection: swipeDirection, pagerScroll: pagerScroll)
- }
- open func move(fromIndex: Int, toIndex: Int, progressPercentage: CGFloat, pagerScroll: PagerScroll) {
- selectedIndex = progressPercentage > 0.5 ? toIndex : fromIndex
- let fromFrame = layoutAttributesForItem(at: IndexPath(item: fromIndex, section: 0))!.frame
- let numberOfItems = dataSource!.collectionView(self, numberOfItemsInSection: 0)
- var toFrame: CGRect
- if toIndex < 0 || toIndex > numberOfItems - 1 {
- if toIndex < 0 {
- let cellAtts = layoutAttributesForItem(at: IndexPath(item: 0, section: 0))
- toFrame = cellAtts!.frame.offsetBy(dx: -cellAtts!.frame.size.width, dy: 0)
- } else {
- let cellAtts = layoutAttributesForItem(at: IndexPath(item: (numberOfItems - 1), section: 0))
- toFrame = cellAtts!.frame.offsetBy(dx: cellAtts!.frame.size.width, dy: 0)
- }
- } else {
- toFrame = layoutAttributesForItem(at: IndexPath(item: toIndex, section: 0))!.frame
- }
- var targetFrame = fromFrame
- targetFrame.size.height = selectedBar.frame.size.height
- targetFrame.size.width += (toFrame.size.width - fromFrame.size.width) * progressPercentage
- targetFrame.origin.x += (toFrame.origin.x - fromFrame.origin.x) * progressPercentage
- selectedBar.frame = CGRect(x: targetFrame.origin.x, y: selectedBar.frame.origin.y, width: targetFrame.size.width, height: selectedBar.frame.size.height)
- var targetContentOffset: CGFloat = 0.0
- if contentSize.width > frame.size.width {
- let toContentOffset = contentOffsetForCell(withFrame: toFrame, andIndex: toIndex)
- let fromContentOffset = contentOffsetForCell(withFrame: fromFrame, andIndex: fromIndex)
- targetContentOffset = fromContentOffset + ((toContentOffset - fromContentOffset) * progressPercentage)
- }
- setContentOffset(CGPoint(x: targetContentOffset, y: 0), animated: false)
- }
- open func updateSelectedBarPosition(_ animated: Bool, swipeDirection: SwipeDirection, pagerScroll: PagerScroll) {
- var selectedBarFrame = selectedBar.frame
- let selectedCellIndexPath = IndexPath(item: selectedIndex, section: 0)
- let attributes = layoutAttributesForItem(at: selectedCellIndexPath)
- let selectedCellFrame = attributes!.frame
- updateContentOffset(animated: animated, pagerScroll: pagerScroll, toFrame: selectedCellFrame, toIndex: (selectedCellIndexPath as NSIndexPath).row)
- selectedBarFrame.size.width = selectedCellFrame.size.width
- selectedBarFrame.origin.x = selectedCellFrame.origin.x
- if animated {
- UIView.animate(withDuration: 0.3, animations: { [weak self] in
- self?.selectedBar.frame = selectedBarFrame
- })
- } else {
- selectedBar.frame = selectedBarFrame
- }
- }
- // MARK: - Helpers
- private func updateContentOffset(animated: Bool, pagerScroll: PagerScroll, toFrame: CGRect, toIndex: Int) {
- guard pagerScroll != .no || (pagerScroll != .scrollOnlyIfOutOfScreen && (toFrame.origin.x < contentOffset.x || toFrame.origin.x >= (contentOffset.x + frame.size.width - contentInset.left))) else { return }
- let targetContentOffset = contentSize.width > frame.size.width ? contentOffsetForCell(withFrame: toFrame, andIndex: toIndex) : 0
- setContentOffset(CGPoint(x: targetContentOffset, y: 0), animated: animated)
- }
- private func contentOffsetForCell(withFrame cellFrame: CGRect, andIndex index: Int) -> CGFloat {
- let sectionInset = (collectionViewLayout as! UICollectionViewFlowLayout).sectionInset // swiftlint:disable:this force_cast
- var alignmentOffset: CGFloat = 0.0
- switch selectedBarAlignment {
- case .left:
- alignmentOffset = sectionInset.left
- case .right:
- alignmentOffset = frame.size.width - sectionInset.right - cellFrame.size.width
- case .center:
- alignmentOffset = (frame.size.width - cellFrame.size.width) * 0.5
- case .progressive:
- let cellHalfWidth = cellFrame.size.width * 0.5
- let leftAlignmentOffset = sectionInset.left + cellHalfWidth
- let rightAlignmentOffset = frame.size.width - sectionInset.right - cellHalfWidth
- let numberOfItems = dataSource!.collectionView(self, numberOfItemsInSection: 0)
- let progress = index / (numberOfItems - 1)
- alignmentOffset = leftAlignmentOffset + (rightAlignmentOffset - leftAlignmentOffset) * CGFloat(progress) - cellHalfWidth
- }
- var contentOffset = cellFrame.origin.x - alignmentOffset
- contentOffset = max(0, contentOffset)
- contentOffset = min(contentSize.width - frame.size.width, contentOffset)
- return contentOffset
- }
- private func updateSelectedBarYPosition() {
- var selectedBarFrame = selectedBar.frame
- switch selectedBarVerticalAlignment {
- case .top:
- selectedBarFrame.origin.y = 0
- case .middle:
- selectedBarFrame.origin.y = (frame.size.height - selectedBarHeight) / 2
- case .bottom:
- selectedBarFrame.origin.y = frame.size.height - selectedBarHeight
- }
- selectedBarFrame.size.height = selectedBarHeight
- selectedBar.frame = selectedBarFrame
- }
- override open func layoutSubviews() {
- super.layoutSubviews()
- updateSelectedBarYPosition()
- }
- }
- open class BaseButtonBarPagerTabStripViewController<ButtonBarCellType: UICollectionViewCell>: PagerTabStripViewController, PagerTabStripDataSource, PagerTabStripIsProgressiveDelegate, UICollectionViewDelegate, UICollectionViewDataSource {
- public var settings = ButtonBarPagerTabStripSettings()
- public var buttonBarItemSpec: ButtonBarItemSpec<ButtonBarCellType>!
- public var changeCurrentIndex: ((_ oldCell: ButtonBarCellType?, _ newCell: ButtonBarCellType?, _ animated: Bool) -> Void)?
- public var changeCurrentIndexProgressive: ((_ oldCell: ButtonBarCellType?, _ newCell: ButtonBarCellType?, _ progressPercentage: CGFloat, _ changeCurrentIndex: Bool, _ animated: Bool) -> Void)?
- @IBOutlet public weak var buttonBarView: ButtonBarView!
- lazy private var cachedCellWidths: [CGFloat]? = { [unowned self] in
- return self.calculateWidths()
- }()
- public override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
- super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
- delegate = self
- datasource = self
- }
- required public init?(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- delegate = self
- datasource = self
- }
- open override func viewDidLoad() {
- super.viewDidLoad()
- let buttonBarViewAux = buttonBarView ?? {
- let flowLayout = UICollectionViewFlowLayout()
- flowLayout.scrollDirection = .horizontal
- let buttonBarHeight = settings.style.buttonBarHeight ?? 44
- let yPos: CGFloat;
- if #available(iOS 11.0, *) {
- yPos = navigationController!.navigationBar.safeAreaInsets.top + navigationController!.navigationBar.frame.height
- } else {
- yPos = navigationController!.navigationBar.frame.maxY
- }
- let buttonBar = ButtonBarView(frame: CGRect(x: 0, y: yPos, width: view.frame.size.width, height: buttonBarHeight), collectionViewLayout: flowLayout)
- buttonBar.backgroundColor = .orange
- buttonBar.selectedBar.backgroundColor = .black
- buttonBar.autoresizingMask = .flexibleWidth
- var newContainerViewFrame = containerView.frame
- newContainerViewFrame.origin.y = buttonBar.frame.maxY
- newContainerViewFrame.size.height = containerView.frame.size.height - (buttonBar.frame.maxY - containerView.frame.origin.y)
- containerView.frame = newContainerViewFrame
- return buttonBar
- }()
- buttonBarView = buttonBarViewAux
- if buttonBarView.superview == nil {
- view.addSubview(buttonBarView)
- }
- if buttonBarView.delegate == nil {
- buttonBarView.delegate = self
- }
- if buttonBarView.dataSource == nil {
- buttonBarView.dataSource = self
- }
- buttonBarView.scrollsToTop = false
- let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
- flowLayout.scrollDirection = .horizontal
- flowLayout.minimumInteritemSpacing = settings.style.buttonBarMinimumInteritemSpacing ?? flowLayout.minimumInteritemSpacing
- flowLayout.minimumLineSpacing = settings.style.buttonBarMinimumLineSpacing ?? flowLayout.minimumLineSpacing
- buttonBarView.showsHorizontalScrollIndicator = false
- buttonBarView.backgroundColor = settings.style.buttonBarBackgroundColor ?? buttonBarView.backgroundColor
- buttonBarView.selectedBar.backgroundColor = settings.style.selectedBarBackgroundColor
- buttonBarView.selectedBarHeight = settings.style.selectedBarHeight
- // register button bar item cell
- switch buttonBarItemSpec! {
- case .nibFile(let nibName, let bundle, _):
- buttonBarView.register(UINib(nibName: nibName, bundle: bundle), forCellWithReuseIdentifier:"Cell")
- case .cellClass:
- buttonBarView.register(ButtonBarCellType.self, forCellWithReuseIdentifier:"Cell")
- }
- }
- open override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- buttonBarView.layoutIfNeeded()
- isViewAppearing = true
- }
- open override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- isViewAppearing = false
- }
- open override func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- guard isViewAppearing || isViewRotating else { return }
- // Force the UICollectionViewFlowLayout to get laid out again with the new size if
- // a) The view is appearing. This ensures that
- // collectionView:layout:sizeForItemAtIndexPath: is called for a second time
- // when the view is shown and when the view *frame(s)* are actually set
- // (we need the view frame's to have been set to work out the size's and on the
- // first call to collectionView:layout:sizeForItemAtIndexPath: the view frame(s)
- // aren't set correctly)
- // b) The view is rotating. This ensures that
- // collectionView:layout:sizeForItemAtIndexPath: is called again and can use the views
- // *new* frame so that the buttonBarView cell's actually get resized correctly
- cachedCellWidths = calculateWidths()
- buttonBarView.collectionViewLayout.invalidateLayout()
- // When the view first appears or is rotated we also need to ensure that the barButtonView's
- // selectedBar is resized and its contentOffset/scroll is set correctly (the selected
- // tab/cell may end up either skewed or off screen after a rotation otherwise)
- buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .scrollOnlyIfOutOfScreen)
- buttonBarView.selectItem(at: IndexPath(item: currentIndex, section: 0), animated: false, scrollPosition: [])
- }
- // MARK: - View Rotation
- open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- let yPos: CGFloat;
- if #available(iOS 11.0, *) {
- yPos = navigationController!.navigationBar.safeAreaInsets.top + navigationController!.navigationBar.frame.height
- } else {
- yPos = navigationController!.navigationBar.frame.maxY
- }
- if let buttonBarView = buttonBarView {
- buttonBarView.frame = CGRect(x: 0, y: yPos, width: view.frame.size.width, height: buttonBarView.frame.height)
- var newContainerViewFrame = containerView.frame
- newContainerViewFrame.origin.y = buttonBarView.frame.maxY
- newContainerViewFrame.size.height = containerView.frame.size.height - (buttonBarView.frame.maxY - containerView.frame.origin.y)
- containerView.frame = newContainerViewFrame
- }
- }
- // MARK: - Public Methods
- open override func reloadPagerTabStripView() {
- super.reloadPagerTabStripView()
- guard isViewLoaded else { return }
- buttonBarView.reloadData()
- cachedCellWidths = calculateWidths()
- buttonBarView.moveTo(index: currentIndex, animated: false, swipeDirection: .none, pagerScroll: .yes)
- }
- open func calculateStretchedCellWidths(_ minimumCellWidths: [CGFloat], suggestedStretchedCellWidth: CGFloat, previousNumberOfLargeCells: Int) -> CGFloat {
- var numberOfLargeCells = 0
- var totalWidthOfLargeCells: CGFloat = 0
- for minimumCellWidthValue in minimumCellWidths where minimumCellWidthValue > suggestedStretchedCellWidth {
- totalWidthOfLargeCells += minimumCellWidthValue
- numberOfLargeCells += 1
- }
- guard numberOfLargeCells > previousNumberOfLargeCells else { return suggestedStretchedCellWidth }
- let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
- let collectionViewAvailiableWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
- let numberOfCells = minimumCellWidths.count
- let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
- let numberOfSmallCells = numberOfCells - numberOfLargeCells
- let newSuggestedStretchedCellWidth = (collectionViewAvailiableWidth - totalWidthOfLargeCells - cellSpacingTotal) / CGFloat(numberOfSmallCells)
- return calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: newSuggestedStretchedCellWidth, previousNumberOfLargeCells: numberOfLargeCells)
- }
- open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int) {
- guard shouldUpdateButtonBarView else { return }
- buttonBarView.moveTo(index: toIndex, animated: true, swipeDirection: toIndex < fromIndex ? .right : .left, pagerScroll: .yes)
- if let changeCurrentIndex = changeCurrentIndex {
- let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)) as? ButtonBarCellType
- let newCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
- changeCurrentIndex(oldCell, newCell, true)
- }
- }
- open func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool) {
- guard shouldUpdateButtonBarView else { return }
- buttonBarView.move(fromIndex: fromIndex, toIndex: toIndex, progressPercentage: progressPercentage, pagerScroll: .yes)
- if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
- let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex != fromIndex ? fromIndex : toIndex, section: 0)) as? ButtonBarCellType
- let newCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
- changeCurrentIndexProgressive(oldCell, newCell, progressPercentage, indexWasChanged, true)
- }
- }
- // MARK: - UICollectionViewDelegateFlowLayut
- @objc open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: IndexPath) -> CGSize {
- guard let cellWidthValue = cachedCellWidths?[indexPath.row] else {
- fatalError("cachedCellWidths for \(indexPath.row) must not be nil")
- }
- return CGSize(width: cellWidthValue, height: collectionView.frame.size.height)
- }
- open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
- guard indexPath.item != currentIndex else { return }
- buttonBarView.moveTo(index: indexPath.item, animated: true, swipeDirection: .none, pagerScroll: .yes)
- shouldUpdateButtonBarView = false
- let oldCell = buttonBarView.cellForItem(at: IndexPath(item: currentIndex, section: 0)) as? ButtonBarCellType
- let newCell = buttonBarView.cellForItem(at: IndexPath(item: indexPath.item, section: 0)) as? ButtonBarCellType
- if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
- changeCurrentIndexProgressive(oldCell, newCell, 1, true, true)
- }
- moveToViewController(at: indexPath.item)
- }
- // MARK: - UICollectionViewDataSource
- open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
- return viewControllers.count
- }
- open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
- guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as? ButtonBarCellType else {
- fatalError("UICollectionViewCell should be or extend from ButtonBarViewCell")
- }
- let childController = viewControllers[indexPath.item] as! IndicatorInfoProvider // swiftlint:disable:this force_cast
- let indicatorInfo = childController.indicatorInfo(for: self)
- configure(cell: cell, for: indicatorInfo)
- if let changeCurrentIndexProgressive = changeCurrentIndexProgressive {
- changeCurrentIndexProgressive(currentIndex == indexPath.item ? nil : cell, currentIndex == indexPath.item ? cell : nil, 1, true, false)
- }
- return cell
- }
- // MARK: - UIScrollViewDelegate
- open override func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
- super.scrollViewDidEndScrollingAnimation(scrollView)
- guard scrollView == containerView else { return }
- shouldUpdateButtonBarView = true
- }
- open func configure(cell: ButtonBarCellType, for indicatorInfo: IndicatorInfo) {
- fatalError("You must override this method to set up ButtonBarView cell accordingly")
- }
- private func calculateWidths() -> [CGFloat] {
- let flowLayout = buttonBarView.collectionViewLayout as! UICollectionViewFlowLayout // swiftlint:disable:this force_cast
- let numberOfCells = viewControllers.count
- var minimumCellWidths = [CGFloat]()
- var collectionViewContentWidth: CGFloat = 0
- for viewController in viewControllers {
- let childController = viewController as! IndicatorInfoProvider // swiftlint:disable:this force_cast
- let indicatorInfo = childController.indicatorInfo(for: self)
- switch buttonBarItemSpec! {
- case .cellClass(let widthCallback):
- let width = widthCallback(indicatorInfo)
- minimumCellWidths.append(width)
- collectionViewContentWidth += width
- case .nibFile(_, _, let widthCallback):
- let width = widthCallback(indicatorInfo)
- minimumCellWidths.append(width)
- collectionViewContentWidth += width
- }
- }
- let cellSpacingTotal = CGFloat(numberOfCells - 1) * flowLayout.minimumLineSpacing
- collectionViewContentWidth += cellSpacingTotal
- let collectionViewAvailableVisibleWidth = buttonBarView.frame.size.width - flowLayout.sectionInset.left - flowLayout.sectionInset.right
- if !settings.style.buttonBarItemsShouldFillAvailableWidth || collectionViewAvailableVisibleWidth < collectionViewContentWidth {
- return minimumCellWidths
- } else {
- let stretchedCellWidthIfAllEqual = (collectionViewAvailableVisibleWidth - cellSpacingTotal) / CGFloat(numberOfCells)
- let generalMinimumCellWidth = calculateStretchedCellWidths(minimumCellWidths, suggestedStretchedCellWidth: stretchedCellWidthIfAllEqual, previousNumberOfLargeCells: 0)
- var stretchedCellWidths = [CGFloat]()
- for minimumCellWidthValue in minimumCellWidths {
- let cellWidth = (minimumCellWidthValue > generalMinimumCellWidth) ? minimumCellWidthValue : generalMinimumCellWidth
- stretchedCellWidths.append(cellWidth)
- }
- return stretchedCellWidths
- }
- }
- private var shouldUpdateButtonBarView = true
- }
- // MARK: Protocols
- public protocol IndicatorInfoProvider {
- func indicatorInfo(for pagerTabStripController: PagerTabStripViewController) -> IndicatorInfo
- }
- public protocol PagerTabStripDelegate: class {
- func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int)
- }
- public protocol PagerTabStripIsProgressiveDelegate: PagerTabStripDelegate {
- func updateIndicator(for viewController: PagerTabStripViewController, fromIndex: Int, toIndex: Int, withProgressPercentage progressPercentage: CGFloat, indexWasChanged: Bool)
- }
- public protocol PagerTabStripDataSource: class {
- func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController]
- }
- // MARK: PagerTabStripViewController
- open class PagerTabStripViewController: UIViewController, UIScrollViewDelegate {
- @IBOutlet weak public var containerView: UIScrollView!
- open weak var delegate: PagerTabStripDelegate?
- open weak var datasource: PagerTabStripDataSource?
- open private(set) var viewControllers = [UIViewController]()
- open private(set) var currentIndex = 0
- open private(set) var preCurrentIndex = 0 // used *only* to store the index to which move when the pager becomes visible
- open var pageWidth: CGFloat {
- return containerView.bounds.width
- }
- open var scrollPercentage: CGFloat {
- if swipeDirection != .right {
- let module = fmod(containerView.contentOffset.x, pageWidth)
- return module == 0.0 ? 1.0 : module / pageWidth
- }
- return 1 - fmod(containerView.contentOffset.x >= 0 ? containerView.contentOffset.x : pageWidth + containerView.contentOffset.x, pageWidth) / pageWidth
- }
- open var swipeDirection: SwipeDirection {
- if containerView.contentOffset.x > lastContentOffset {
- return .left
- } else if containerView.contentOffset.x < lastContentOffset {
- return .right
- }
- return .none
- }
- override open func viewDidLoad() {
- super.viewDidLoad()
- let containerViewAux = containerView ?? {
- let containerView = UIScrollView(frame: CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height))
- containerView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
- return containerView
- }()
- containerView = containerViewAux
- if containerView.superview == nil {
- view.addSubview(containerView)
- }
- containerView.bounces = true
- containerView.alwaysBounceHorizontal = true
- containerView.alwaysBounceVertical = false
- containerView.scrollsToTop = false
- containerView.delegate = self
- containerView.showsVerticalScrollIndicator = false
- containerView.showsHorizontalScrollIndicator = false
- containerView.isPagingEnabled = true
- containerView.backgroundColor = PresentationTheme.current.colors.background
- reloadViewControllers()
- let childController = viewControllers[currentIndex]
- addChildViewController(childController)
- childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- containerView.addSubview(childController.view)
- childController.didMove(toParentViewController: self)
- }
- open override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- isViewAppearing = true
- childViewControllers.forEach { $0.beginAppearanceTransition(true, animated: animated) }
- }
- override open func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- lastSize = containerView.bounds.size
- updateIfNeeded()
- let needToUpdateCurrentChild = preCurrentIndex != currentIndex
- if needToUpdateCurrentChild {
- moveToViewController(at: preCurrentIndex)
- }
- isViewAppearing = false
- childViewControllers.forEach { $0.endAppearanceTransition() }
- }
- open override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
- childViewControllers.forEach { $0.beginAppearanceTransition(false, animated: animated) }
- }
- open override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- childViewControllers.forEach { $0.endAppearanceTransition() }
- }
- override open func viewDidLayoutSubviews() {
- super.viewDidLayoutSubviews()
- updateIfNeeded()
- }
- open override var shouldAutomaticallyForwardAppearanceMethods: Bool {
- return false
- }
- open func moveToViewController(at index: Int, animated: Bool = true) {
- guard isViewLoaded && view.window != nil && currentIndex != index else {
- preCurrentIndex = index
- return
- }
- if animated && abs(currentIndex - index) > 1 {
- var tmpViewControllers = viewControllers
- let currentChildVC = viewControllers[currentIndex]
- let fromIndex = currentIndex < index ? index - 1 : index + 1
- let fromChildVC = viewControllers[fromIndex]
- tmpViewControllers[currentIndex] = fromChildVC
- tmpViewControllers[fromIndex] = currentChildVC
- pagerTabStripChildViewControllersForScrolling = tmpViewControllers
- containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: fromIndex), y: 0), animated: false)
- (navigationController?.view ?? view).isUserInteractionEnabled = !animated
- containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: true)
- } else {
- (navigationController?.view ?? view).isUserInteractionEnabled = !animated
- containerView.setContentOffset(CGPoint(x: pageOffsetForChild(at: index), y: 0), animated: animated)
- }
- }
- open func moveTo(viewController: UIViewController, animated: Bool = true) {
- moveToViewController(at: viewControllers.index(of: viewController)!, animated: animated)
- }
- // MARK: - PagerTabStripDataSource
- open func viewControllers(for pagerTabStripController: PagerTabStripViewController) -> [UIViewController] {
- assertionFailure("Sub-class must implement the PagerTabStripDataSource viewControllers(for:) method")
- return []
- }
- // MARK: - Helpers
- public enum PagerTabStripError: Error {
- case viewControllerOutOfBounds
- }
-
- open func updateIfNeeded() {
- if isViewLoaded && !lastSize.equalTo(containerView.bounds.size) {
- updateContent()
- }
- }
- open func canMoveTo(index: Int) -> Bool {
- return currentIndex != index && viewControllers.count > index
- }
- open func pageOffsetForChild(at index: Int) -> CGFloat {
- return CGFloat(index) * containerView.bounds.width
- }
- open func offsetForChild(at index: Int) -> CGFloat {
- return (CGFloat(index) * containerView.bounds.width) + ((containerView.bounds.width - view.bounds.width) * 0.5)
- }
- open func offsetForChild(viewController: UIViewController) throws -> CGFloat {
- guard let index = viewControllers.index(of: viewController) else {
- throw PagerTabStripError.viewControllerOutOfBounds
- }
- return offsetForChild(at: index)
- }
- open func pageFor(contentOffset: CGFloat) -> Int {
- let result = virtualPageFor(contentOffset: contentOffset)
- return pageFor(virtualPage: result)
- }
- open func virtualPageFor(contentOffset: CGFloat) -> Int {
- return Int((contentOffset + 1.5 * pageWidth) / pageWidth) - 1
- }
- open func pageFor(virtualPage: Int) -> Int {
- if virtualPage < 0 {
- return 0
- }
- if virtualPage > viewControllers.count - 1 {
- return viewControllers.count - 1
- }
- return virtualPage
- }
- open func updateContent() {
- if lastSize.width != containerView.bounds.size.width {
- lastSize = containerView.bounds.size
- containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
- }
- lastSize = containerView.bounds.size
- let pagerViewControllers = pagerTabStripChildViewControllersForScrolling ?? viewControllers
- containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(pagerViewControllers.count), height: containerView.contentSize.height)
- for (index, childController) in pagerViewControllers.enumerated() {
- let pageOffsetForChild = self.pageOffsetForChild(at: index)
- if fabs(containerView.contentOffset.x - pageOffsetForChild) < containerView.bounds.width {
- if childController.parent != nil {
- childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
- childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- } else {
- childController.beginAppearanceTransition(true, animated: false)
- addChildViewController(childController)
- childController.view.frame = CGRect(x: offsetForChild(at: index), y: 0, width: view.bounds.width, height: containerView.bounds.height)
- childController.view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
- containerView.addSubview(childController.view)
- childController.didMove(toParentViewController: self)
- childController.endAppearanceTransition()
- }
- } else {
- if childController.parent != nil {
- childController.beginAppearanceTransition(false, animated: false)
- childController.willMove(toParentViewController: nil)
- childController.view.removeFromSuperview()
- childController.removeFromParentViewController()
- childController.endAppearanceTransition()
- }
- }
- }
- let oldCurrentIndex = currentIndex
- let virtualPage = virtualPageFor(contentOffset: containerView.contentOffset.x)
- let newCurrentIndex = pageFor(virtualPage: virtualPage)
- currentIndex = newCurrentIndex
- preCurrentIndex = currentIndex
- let changeCurrentIndex = newCurrentIndex != oldCurrentIndex
- if let progressiveDelegate = self as? PagerTabStripIsProgressiveDelegate {
- let (fromIndex, toIndex, scrollPercentage) = progressiveIndicatorData(virtualPage)
- progressiveDelegate.updateIndicator(for: self, fromIndex: fromIndex, toIndex: toIndex, withProgressPercentage: scrollPercentage, indexWasChanged: changeCurrentIndex)
- } else {
- delegate?.updateIndicator(for: self, fromIndex: min(oldCurrentIndex, pagerViewControllers.count - 1), toIndex: newCurrentIndex)
- }
- }
- open func reloadPagerTabStripView() {
- guard isViewLoaded else { return }
- for childController in viewControllers where childController.parent != nil {
- childController.beginAppearanceTransition(false, animated: false)
- childController.willMove(toParentViewController: nil)
- childController.view.removeFromSuperview()
- childController.removeFromParentViewController()
- childController.endAppearanceTransition()
- }
- reloadViewControllers()
- containerView.contentSize = CGSize(width: containerView.bounds.width * CGFloat(viewControllers.count), height: containerView.contentSize.height)
- if currentIndex >= viewControllers.count {
- currentIndex = viewControllers.count - 1
- }
- preCurrentIndex = currentIndex
- containerView.contentOffset = CGPoint(x: pageOffsetForChild(at: currentIndex), y: 0)
- updateContent()
- }
- // MARK: - UIScrollViewDelegate
- open func scrollViewDidScroll(_ scrollView: UIScrollView) {
- if containerView == scrollView {
- updateContent()
- lastContentOffset = scrollView.contentOffset.x
- }
- }
- open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
- if containerView == scrollView {
- lastPageNumber = pageFor(contentOffset: scrollView.contentOffset.x)
- }
- }
- open func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
- if containerView == scrollView {
- pagerTabStripChildViewControllersForScrolling = nil
- (navigationController?.view ?? view).isUserInteractionEnabled = true
- updateContent()
- }
- }
- // MARK: - Orientation
- open override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
- super.viewWillTransition(to: size, with: coordinator)
- isViewRotating = true
- pageBeforeRotate = currentIndex
- coordinator.animate(alongsideTransition: nil) { [weak self] _ in
- guard let me = self else { return }
- me.isViewRotating = false
- me.currentIndex = me.pageBeforeRotate
- me.preCurrentIndex = me.currentIndex
- me.updateIfNeeded()
- }
- }
- // MARK: Private
- private func progressiveIndicatorData(_ virtualPage: Int) -> (Int, Int, CGFloat) {
- let count = viewControllers.count
- var fromIndex = currentIndex
- var toIndex = currentIndex
- let direction = swipeDirection
- if direction == .left {
- if virtualPage > count - 1 {
- fromIndex = count - 1
- toIndex = count
- } else {
- if self.scrollPercentage >= 0.5 {
- fromIndex = max(toIndex - 1, 0)
- } else {
- toIndex = fromIndex + 1
- }
- }
- } else if direction == .right {
- if virtualPage < 0 {
- fromIndex = 0
- toIndex = -1
- } else {
- if self.scrollPercentage > 0.5 {
- fromIndex = min(toIndex + 1, count - 1)
- } else {
- toIndex = fromIndex - 1
- }
- }
- }
- return (fromIndex, toIndex, self.scrollPercentage)
- }
- private func reloadViewControllers() {
- guard let dataSource = datasource else {
- fatalError("dataSource must not be nil")
- }
- viewControllers = dataSource.viewControllers(for: self)
- // viewControllers
- guard !viewControllers.isEmpty else {
- fatalError("viewControllers(for:) should provide at least one child view controller")
- }
- viewControllers.forEach { if !($0 is IndicatorInfoProvider) { fatalError("Every view controller provided by PagerTabStripDataSource's viewControllers(for:) method must conform to IndicatorInfoProvider") }}
- }
- private var pagerTabStripChildViewControllersForScrolling: [UIViewController]?
- private var lastPageNumber = 0
- private var lastContentOffset: CGFloat = 0.0
- private var pageBeforeRotate = 0
- private var lastSize = CGSize(width: 0, height: 0)
- internal var isViewRotating = false
- internal var isViewAppearing = false
- }
|