iOS UIPageViewController 变得混乱并且不显示 ViewControllers

iOS UIPageViewController Gets Confused and ViewControllers Are Not Displayed

我们设置了左右按钮,以便用户快速翻阅不同的汽车。如果用户快速点击下一页 10 次或更多次,我们的页面视图控制器将丢失视图控制器。

这是正确显示汽车的车辆页面(模糊以隐藏不相关的信息)。在这里查看图片:

如果滚动动画开启 (true),它会在快速点击向右箭头 6 次或更多次后丢失车辆页面。在这里查看图片:

代码:

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
    let viewControllers = [viewController]
    let isAnimated = true // false always works. However, animation is required.
    setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: nil)
}

调试时,当页面视图控制器停止显示汽车时,我确保设置的视图控制器不为零,列表(汽车)也不为零。

我尝试了 UIPageViewController, how do I correctly jump to a specific page without messing up the order specified by the data source? 中使用完成块的解决方案的变体。但是,它没有用。

weak var pvcw: UIPageViewController? = self
setViewControllers(viewControllers, direction: direction, animated: true, completion: {(_ finished: Bool) -> Void in
    let pvcs: UIPageViewController? = pvcw
    if pvcs == nil {
        return
    }
    DispatchQueue.main.async(execute: {() -> Void in
        pvcs?.setViewControllers(viewControllers, direction: direction, animated: false) {(_ finished: Bool) -> Void in }
    })
})

有什么想法吗?谢谢。

更新

我注意到有时包含的视图控制器可能会偏离中心而不是完全缺失。

我更深入地研究了视图控制器完全丢失的情况。单击 "Debug View Hierarchy" 并打开“Show Clipped Content”会在视图控制器完全丢失时显示以下内容:

因此,缺失的内容似乎被剪裁/越界了。

仅显示线框图显示以下内容:

页面视图控制器有一个

我还看到 _UIQueuingScrollView 的边界在情况很奇怪时有很大不同。当一切正常时,边界的 x 为 1125,而 X 为 375。

只有在使用滚动过渡样式而不是卷页时才会发生这种情况。使用 Page Curl 时,一切正常。

我们如何预防/解决这个问题?

第二次更新

此代码使问题消失。然而,它留下了更刺耳的体验。可能是延迟了0.4秒,正常使用时有时会出现蓝色背景

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {

    let viewControllers = [viewController]
    setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
            self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
        })
    })
}

这不是一个好的用户体验。 有没有更好的方法?

我希望滚动过渡平滑,不要短暂显示蓝色背景,并且不要丢失其内容,即视图控制器内容。

虽然真正的答案是让视图控制器尽可能简单(但并不简单),但这里的代码解决了当用户导航到下一个视图控制器。

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {

    let viewControllers = [viewController]
    setViewControllers(viewControllers, direction: direction, animated: true, completion: { (_) in

        DispatchQueue.main.asyncAfter(deadline: .now() + 0.4, execute: {
            self.setViewControllers(viewControllers, direction: direction, animated: false, completion: nil)
        })
    })
}

我不确定此解决方案是否适合您的用户,但如果由于用户快速导航而出现问题,您可以实施锁定以禁止此快速导航。本质上:

private func show(viewController:UIViewController, going direction: UIPageViewControllerNavigationDirection) {
    guard !isChangingPages else { return }
    isChangingPages = true
    let viewControllers = [viewController]
    let isAnimated = true // false always works. However, animation is required.
    setViewControllers(viewControllers, direction: direction, animated: isAnimated, completion: { [weak self] _ in 
        self?.isChangingPages = false
    })
}

这样,您必须先完成到新页面的转换,然后才能转换到下一页。

如果您在将此布尔值设置为 true 时保持导航按钮处于启用状态(点击看不到结果),这可能会导致用户感到困惑。但是可以更改逻辑以禁用按钮并在完成块中重新启用它们(这样它们会在页面更改期间消失 in/out)。

一个简单的解决方案是通过添加一个小 "tap ahead" 缓冲区将按钮点击与视图控制器更改分离。创建一个按钮队列(使用一个简单的 NSMutableArray 作为 FIFO 队列),在其中添加每个导航按钮点击,然后调用出队函数 如果 在添加之前队列为空.

在出队函数中,您删除第一个条目并相应地更改视图,然后在 setViewControllers 完成处理程序中再次调用自身 如果 队列不为空。

确保只在主线程上进行处理以避免线程问题。如果需要,您还可以对允许的数量添加限制 "tap ahead",并可能在方向更改时刷新队列。

您好,我创建了一个 Sample Project,应该可以解决您的问题。我已经添加了 100 ViewControllers(通过循环)并且它在滚动动画中工作正常。事情就在他们的地方。

我在这个项目中所做的是:

  1. 为具有两个属性的页面创建了一个基类

    a) Int 类型的 pageIndex

    b) 回调协议委托

  2. 通过 ContainerView

  3. 将 UIPageViewController 添加到 ViewController
  4. 创建了一个 ViewController 扩展 PageViewBase 的命名页面

  5. 运行 循环计数为 100 并将数据添加到数组并将数据源和委托设置为自身 (PageControlelr) 并根据 pageIndex 属性[=15 进行管理=]

页ViewController

class PageViewController: UIPageViewController {
    var list = [Page]()
    var sb: UIStoryboard?
    var viewController: ViewController! // settting from ViewController
    override func viewDidLoad() {
        super.viewDidLoad()
        sb = UIStoryboard(name: "Main", bundle: nil)
        DispatchQueue.main.asyncAfter(deadline: .now()+0.4, execute: {
            self.setupList()
        })
    }
    func setupList(){
        for i in 0..<100{
            let model = PageModel(title: "Title \(i + 1)", subTitle: "SubTitle \(i + 1)")
            let page = sb?.instantiateViewController(withIdentifier: "PageID") as! Page
            page.data = model
            page.pageIndex = i
            page.delegate = viewController
            list.append(page)
        }
        self.delegate = self
        self.dataSource = self
        setViewControllers([list[0]], direction: .forward, animated: true, completion: nil)
        self.updateCurrentPageLabel(index: 0)
    }
    func movePage(index: Int){
        let currentIndex = self.viewControllers![0] as! Page
        self.updateCurrentPageLabel(index: index)
        setViewControllers([list[index]], direction: index > currentIndex.pageIndex ? .forward : .reverse, animated: true)
    }
    func getCurrentPageIndex() -> Int{
        return (self.viewControllers![0] as! Page).pageIndex
    }
    func updateCurrentPageLabel(index: Int){
        (self.parent as? ViewController)?.currentListingLabel.text = "\(index + 1) of \(list.count)"
    }
}
extension PageViewController: UIPageViewControllerDelegate{
    func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
        let currentIndex = (self.viewControllers![0] as! Page).pageIndex
        self.updateCurrentPageLabel(index: currentIndex)

    }
}
extension PageViewController: UIPageViewControllerDataSource{
    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        let index = (viewController as! Page).pageIndex
        if index > 0 {
            return list[index-1]
        }
        return nil
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        let index = (viewController as! Page).pageIndex
        if index < list.count-1 {
            return list[index+1]
        }
        return nil
    }

}

import UIKit

struct PageModel {
    var title: String
    var subTitle: String
}

class Page: PageViewBase {
    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var subTitleLabel: UILabel!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet weak var btnWorking: UIButton!
    var data: PageModel?
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTags()
        setupActions()
        setupData()
    }
    func setupData(){
        if let data = data{
            self.titleLabel.text = data.title
            self.subTitleLabel.text = data.subTitle
            imageView.image = #imageLiteral(resourceName: "car")
        }

    }

    enum buttonTags: Int{
        case working = 1
    }
    func setupTags(){
        btnWorking.tag = buttonTags.working.rawValue
    }
    func setupActions(){
        btnWorking.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
    }
    @objc func didSelect(_ sender: UIView){
        if let tag = buttonTags.init(rawValue: sender.tag){
            switch tag{
            case .working:
                delegate?.didReceive(withMessage: "wokring button clicked of index \(pageIndex)")
            }
        }
    }
}

ViewController // 主控制器

import UIKit
protocol CallBack {
    func didReceive(withMessage message: String)
}
class ViewController: UIViewController {
    @IBOutlet weak var containerView: UIView!
    @IBOutlet weak var btnCall: UIButton!
    @IBOutlet weak var btnMessage: UIButton!
    @IBOutlet weak var btnNext: UIButton!
    @IBOutlet weak var btnBack: UIButton!
    @IBOutlet weak var currentListingLabel: UILabel!
    var pageController: PageViewController?

    override func viewDidLoad() {
        super.viewDidLoad()
        setupTags()
        setupActions()
        setupContainerView()
    }

    enum buttonTags: Int{
        case call = 1
        case message
        case next
        case back
    }
    func setupTags(){
        btnCall.tag = buttonTags.call.rawValue
        btnMessage.tag = buttonTags.message.rawValue
        btnNext.tag = buttonTags.next.rawValue
        btnBack.tag = buttonTags.back.rawValue
    }
    func setupActions(){
        btnCall.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
        btnMessage.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
        btnNext.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
        btnBack.addTarget(self, action: #selector(self.didSelect(_:)), for: .touchUpInside)
    }
    @objc func didSelect(_ sender: UIView){
        if let tag = buttonTags.init(rawValue: sender.tag){
            switch tag{
            case .call:
                print("Call button called for index \(pageController?.getCurrentPageIndex() ?? 0)")
            case .message:
                print("message button called for index  \(pageController?.getCurrentPageIndex() ?? 0)")
            case .next:
                if let p = pageController{
                    let currentIndex = p.getCurrentPageIndex()
                    if currentIndex < p.list.count - 1{
                        p.movePage(index: currentIndex + 1)
                    }
                }
            case .back:
                if let p = pageController{
                    let currentIndex = p.getCurrentPageIndex()
                    if currentIndex > 0{
                        p.movePage(index: currentIndex - 1)
                    }
                }
            }
        }
    }
    func setupContainerView(){
        let sb = UIStoryboard(name: "Main", bundle: nil)
        pageController = sb.instantiateViewController(withIdentifier: "PageViewControllerID") as? PageViewController
        pageController?.viewController = self
        addViewIntoParentViewController(vc: pageController)
    }
    func addViewIntoParentViewController(vc: UIViewController?){
        if let vc = vc{
            for v in self.containerView.subviews{
                v.removeFromSuperview()
            }
            self.containerView.addSubview(vc.view)
            self.containerView.translatesAutoresizingMaskIntoConstraints = false
            vc.view.translatesAutoresizingMaskIntoConstraints = false
            addChildViewController(vc)
            NSLayoutConstraint.activate([
                vc.view.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                vc.view.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
                vc.view.topAnchor.constraint(equalTo: containerView.topAnchor),
                vc.view.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
                ])
            vc.didMove(toParentViewController: self)
        }
    }
}
extension ViewController: CallBack{
    func didReceive(withMessage message: String) {
        print("message: \(message)")
    }
}

PageViewBase

import UIKit

class PageViewBase: UIViewController {
    var pageIndex = -1
    var delegate: CallBack?
}