可靠地跟踪UIPageViewController(Swift)中的Page Index

问题描述 投票:1回答:2

问题:

我有一个大师UIPageViewControllerMainPageVC)有三个嵌入页面视图(AB,和C),可以通过滑动手势和按下MainPageVC中的自定义页面指示器*中的适当位置(*不是真正的UIPageControl,但是由三个ToggleButtons组成 - 一个简单的UIButton重新实现,成为一个切换按钮)。我的设置如下:

Schematic of my view hierarchy

上一篇文章:Reliable way to track Page Index in a UIPageViewController - SwiftA reliable way to get UIPageViewController current indexUIPageViewController: return the current visible view表示,最好的方法是使用didFinishAnimating调用,并手动跟踪当前页面索引,但我发现这并不涉及某些边缘情况。

我一直在尝试生成一种安全的方式来跟踪当前页面索引(使用didFinishAnimatingwillTransitionTo方法),但是遇到用户在视图A中的边缘情况有问题,然后一直滑到C(没有提起他们的手指),然后超越C,然后释放他们的手指...在这种情况下didFinishAnimating没有被调用,应用程序仍然认为它在A(即A切换按钮仍然按下并且pageIndex未更新正确的viewControllerBeforeviewControllerAfter方法)。

我的代码:

@IBOutlet weak var pagerView: UIView!
@IBOutlet weak var aButton: ToggleButton!
@IBOutlet weak var bButton: ToggleButton!
@IBOutlet weak var cButton: ToggleButton!

let viewControllerNames = ["aVC", "bVC", "cVC"]
lazy var buttonsArray = {
    [aButton, bButton, cButton]
}()
var previousPage = "aVC"

var pageVC: UIPageViewController?

func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
    print("TESTING - will transition to")

    let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder);
    let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass);

    if currentViewControllerClass == previousPage {
        return
    }

    let pastIndex = viewControllerNames.index(of: previousPage)
    if buttonsArray[pastIndex!]?.isOn == true {
        buttonsArray[pastIndex!]?.buttonPressed()
    }

    if let newPageButton = buttonsArray[viewControllerIndex!] {
        newPageButton.buttonPressed()
    }

    self.previousPage = currentViewControllerClass
}

func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
    print("TESTING - did finish animating")

    let currentViewControllerClass = String(describing: pageViewController.viewControllers![0].classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: currentViewControllerClass)

    if currentViewControllerClass == previousPage {
        return
    }

    let pastIndex = viewControllerNames.index(of: previousPage)
    if buttonsArray[pastIndex!]?.isOn == true {
        buttonsArray[pastIndex!]?.buttonPressed()
    }

    if let newPageButton = buttonsArray[viewControllerIndex!] {
        newPageButton.buttonPressed()
    }

    self.previousPage = currentViewControllerClass
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    let onboardingViewControllerClass = String(describing: viewController.classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
    let newViewControllerIndex = viewControllerIndex! - 1
    if(newViewControllerIndex < 0) {
        return nil
    } else {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
        if let vc = vc as? BaseTabVC {
            vc.mainPageVC = self
            vc.intendedCollectionViewHeight = pagerViewHeight
        }
        return vc
    }
}

func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    let onboardingViewControllerClass = String(describing: viewController.classForCoder)
    let viewControllerIndex = viewControllerNames.index(of: onboardingViewControllerClass)
    let newViewControllerIndex = viewControllerIndex! + 1
    if(newViewControllerIndex > viewControllerNames.count - 1) {
        return nil
    } else {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let vc = storyboard.instantiateViewController(withIdentifier: viewControllerNames[newViewControllerIndex])
        if let vc = vc as? BaseTabVC {
            vc.mainPageVC = self
            vc.intendedCollectionViewHeight = pagerViewHeight
        }
        return vc
    }
}

我不知道如何处理这个边缘情况,问题是,如果用户然后试图在C中按某些应该保证存在的东西,并且意外,它可能导致应用程序的致命崩溃nilindexOutOfBounds错误被抛出。

ios swift uipageviewcontroller uipagecontrol
2个回答
1
投票

写得很好的问题。特别是对于新手。 (投票。)您清楚地说明了您遇到的问题,包括插图和您当前的代码。

我在另一个线程中提出的解决方案是继承UIPageControl并让它在其currentPage属性上实现didSet。然后,您可以让页面控件通知视图控制器当前页面索引。 (通过发送通知中心消息或任何最适合您需求的方法,为您的自定义子类提供委托属性。)

(我对这种方法做了一个简单的测试,但它确实有效。但我并没有详尽地测试过。)

事实上,UIPageViewController可靠地更新页面控件但是没有可靠,明显的方法来计算当前页面索引,这似乎是对这个类的设计的疏忽。


0
投票

自己的解决方案

我找到了解决方案:不要使用UIPageView(Controller),而是使用CollectionView(Controller)。跟踪集合视图的位置比尝试手动跟踪UIPageViewController中的当前页面要容易得多。

解决方案如下:

方法

  • MainPagerVC重构为CollectionView(控制器)(或作为符合UICollectionViewDelegate UICollectionViewDataSource协议的常规VC)。
  • 将每个页面(aVCbVCcVC)设置为UICollectionViewCell子类(MainCell)。
  • 设置每个页面以填充屏幕边界内的MainPagerVC.collectionView - CGSize(width: view.frame.width, height: collectionView.bounds.height)
  • 将顶部的切换按钮(ABC)重构为UICollectionViewCell(本身为MenuCell)中的三个MenuController子类(UICollectionViewController)。
  • 当集合视图继承自UIScrollView时,您可以实现scrollViewDidScrollscrollViewDidEndScrollingAnimationscrollViewWillEndDragging方法,以及委托(使用didSelectItemAt indexPath)来耦合MainPagerVCMenuController集合视图。

class MainPagerVC: UIViewController, UICollectionViewDelegateFlowLayout {

    fileprivate let menuController = MenuVC(collectionViewLayout: UICollectionViewFlowLayout())
    fileprivate let cellId = "cellId"

    fileprivate let pages = ["aVC", "bVC", "cVC"]

    let collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.minimumLineSpacing = 0
        layout.scrollDirection = .horizontal
        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.backgroundColor = .white
        cv.showsVerticalScrollIndicator = false
        cv.showsHorizontalScrollIndicator = false
        return cv
    }()


    override func viewDidLoad() {
        super.viewDidLoad()

        menuController.delegate = self

        setupLayout()
    }

    fileprivate func setupLayout() {
        guard let menuView = menuController.view else { return }

        view.addSubview(menuView)
        view.addSubview(collectionView)

        collectionView.dataSource = self
        collectionView.delegate = self


        //Setup constraints (placing the menuView above the collectionView

        collectionView.register(MainCell.self, forCellWithReuseIdentifier: cellId)

        //Make the collection view behave like a pager view (no overscroll, paging enabled)
        collectionView.isPagingEnabled = true
        collectionView.bounces = false
        collectionView.allowsSelection = true

        menuController.collectionView.selectItem(at: [0, 0], animated: true, scrollPosition: .centeredHorizontally)

    }

}

extension MainPagerVC: MenuVCDelegate {

    // Delegate method implementation (scroll to the right page when the corresponding Menu "Button"(Item) is pressed
    func didTapMenuItem(indexPath: IndexPath) {
        collectionView.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
    }

}

extension MainPagerVC: UICollectionViewDelegate, UICollectionViewDataSource {

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let x = scrollView.contentOffset.x
        let offset = x / pages.count
        menuController.menuBar.transform = CGAffineTransform(translationX: offset, y: 0)
    }

    func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        let item = Int(scrollView.contentOffset.x / view.frame.width)
        let indexPath = IndexPath(item: item, section: 0)
        collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .bottom)
    }

    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let x = targetContentOffset.pointee.x
        let item = Int(x / view.frame.width)
        let indexPath = IndexPath(item: item, section: 0)
        menuController.collectionView.selectItem(at: indexPath, animated: true, scrollPosition: .centeredHorizontally)
    }


    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return pages.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MainCell

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return .init(width: view.frame.width, height: collectionView.bounds.height)
    }

}

class MainCell: UICollectionViewCell {

    override init(frame: CGRect) {
        super.init(frame: frame)

        // Custom UIColor extension to return a random colour (to check that everything is working)
        backgroundColor = UIColor().random()

    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }
}

protocol MenuVCDelegate {
    func didTapMenuItem(indexPath: IndexPath)
}

class MenuVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {

    fileprivate let cellId = "cellId"
    fileprivate let menuItems = ["A", "B", "C"]

    var delegate: MenuVCDelegate?

    //Sliding bar indicator (slightly different from original question - like Reddit)
    let menuBar: UIView = {
        let v = UIView()
        v.backgroundColor = .red
        return v
    }()

    //1px view to visually separate MenuBar region from "pager"-views
    let menuSeparator: UIView = {
        let v = UIView()
        v.backgroundColor = .gray
        return v
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.backgroundColor = .white
        collectionView.allowsSelection = true
        collectionView.register(MenuCell.self, forCellWithReuseIdentifier: cellId)

        if let layout = collectionViewLayout as? UICollectionViewFlowLayout {
            layout.scrollDirection = .horizontal
            layout.minimumLineSpacing = 0
            layout.minimumInteritemSpacing = 0
        }

        //Add views and setup constraints for collection view, separator view and "selection indicator" view - the menuBar
    }

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        delegate?.didTapMenuItem(indexPath: indexPath)
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return menuItems.count
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MenuCell
        cell.label.text = menuItems[indexPath.item]

        return cell
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = view.frame.width
        return .init(width: width/CGFloat(menuItems.count), height: view.frame.height)
    }

}

class MenuCell: UICollectionViewCell {

    let label: UILabel = {
        let l = UILabel()
        l.text = "Menu Item"
        l.textAlignment = .center
        l.textColor = .gray
        return l
    }()

    override var isSelected: Bool {
        didSet {
            label.textColor = isSelected ? .black : .gray
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        //Add label to view and setup constraints to fill Cell
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError()
    }
}

参考

  1. 一个“Lets Build the App”YouTube视频:"We Made It on /r/iosprogramming! Live coding swiping pages feature"
© www.soinside.com 2019 - 2024. All rights reserved.