【问题标题】:Reliably track Page Index in a UIPageViewController (Swift)在 UIPageViewController (Swift) 中可靠地跟踪页面索引
【发布时间】:2018-11-23 12:37:19
【问题描述】:

问题:

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

以前的阅读: 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 切换按钮仍处于按下状态,并且viewControllerBeforeviewControllerAfter 方法未正确更新pageIndex)。

我的代码:

@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


    【解决方案1】:

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

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

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

    UIPageViewController 可靠地更新页面控件但没有可靠、明显的方法来确定当前页面索引这一事实似乎是此类设计中的疏忽。

    【讨论】:

    • 感谢您的建议,我会试一试并回复您。我开始尝试防止“过度滚动”UIPageViewController 内的视图,但无济于事。这可能是我的经验不足/我的搜索措辞不正确,因为我不断地指向UIScrollView 的反弹属性(不相关)。您是否认为尝试禁用向左(在最右边的页面上)滑动手势(反之亦然)也可以充分消除这种边缘情况?
    • 我找到了解决方案,不使用UIPageViewController 而是使用UICollectionViewController(因为它的底层scrollView 可用于计算“页面”位置)-我把我更新的解决方案作为答案。谢谢你帮助我帮助自己,邓肯 :)
    【解决方案2】:

    自己的解决方案

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

    解决方法如下:

    方法

    • MainPagerVC 重构为 CollectionView(Controller)(或作为符合 UICollectionViewDelegate UICollectionViewDataSource 协议的常规 VC)。
    • 将每个页面(aVCbVCcVC)设置为 UICollectionViewCell 子类 (MainCell)。
    • 设置每个页面以填充屏幕边界内的MainPagerVC.collectionView - CGSize(width: view.frame.width, height: collectionView.bounds.height)
    • 将顶部的切换按钮(ABC)重构为 MenuController 中的三个 UICollectionViewCell 子类 (MenuCell)(本身是 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. “让我们构建那个应用” YouTube 视频:"We Made It on /r/iosprogramming! Live coding swiping pages feature"

    【讨论】:

    • 很好的解决问题的方法,我只有一个问题,当你滚动浏览VIewControllers时,是否调用了每个ViewController的viewDidAppear或viewDidDisappear方法?还是 ViewController 仅在开始时实例化?
    • @Guala,感谢您的评论!好吧,因为每个页面(aVCbVCcVC)都是 UICollectionViewCell 子类(MainCell),所以它们遵循 iOS 标准 UICollectionViewCell 生命周期(即重用) - 而不是 UIViewController那些。 MainPagerVC,带有collectionView 的封闭视图控制器,仍然会经历这些生命周期事件。但是,您可以向MainCell 添加一个公共方法,比如.configure(_viewModel: MainCellViewModel),并在单元格出列后在cellForItemAt indexPath 中调用它(用于更新)。和MainCell.init(...) 用于布置子视图。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-04-25
    • 2013-12-07
    • 2011-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多