You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI UIPageViewController追加控制器时崩溃与滑动故障修复

问题根因

你碰到的双滑动异常、_flushViewController崩溃本质是同一个问题:UIPageViewController内部有非常严格的状态机限制,转场动画执行期间绝对不能调用setViewControllers,也不能修改其数据源读取的控制器数组。你的现有实现存在3个核心冲突:

  1. 同时实现了无限轮播逻辑(滑到最后一页返回第一页控制器)和滑动加载更多逻辑,两个逻辑直接冲突,导致滑动到边界时UIPageViewController同时收到「回到第一页」和「追加新页」两个指令,出现来回滑动的异常
  2. updateUIViewController会在每次SwiftUI刷新时无脑调用setViewControllers,快速滑动时转场还没结束就触发重复设置,直接触发内部断言崩溃
  3. 在转场结束的delegate回调里同步修改控制器数组,此时UIPageViewController还没完成内部视图清理,修改数据源会导致状态不一致

修复步骤

1. 给Coordinator增加转场状态标记,拦截转场过程中的非法操作

在Coordinator中新增转场状态标记,实现willTransitionTo代理方法监听转场开始,转场结束后重置标记,所有修改控制器、调用setViewControllers的操作都要先判断是否处于转场中。
同时删掉和加载更多冲突的无限轮播逻辑,滑到页面边界时返回nil即可。

class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate {
    var parent: PageViewController
    var controllers = [UIViewController]()
    // 新增:转场状态锁
    var isTransitioning = false

    init(_ pageViewController: PageViewController) {
        parent = pageViewController
        controllers = parent.pages.map { UIHostingController(rootView: $0.ignoresSafeArea()) }
    }

    // 新增:转场开始时加锁
    func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
        isTransitioning = true
    }

    func pageViewController(
        _ pageViewController: UIPageViewController,
        viewControllerBefore viewController: UIViewController) -> UIViewController?
    {
        guard let index = controllers.firstIndex(of: viewController) else {
            return nil
        }
        if index == 0 {
            // 纵向feed流不需要循环滚动,直接返回nil
            return nil
        }
        return controllers[index - 1]
    }

    func pageViewController(
        _ pageViewController: UIPageViewController,
        viewControllerAfter viewController: UIViewController) -> UIViewController?
    {
        guard let index = controllers.firstIndex(of: viewController) else {
            return nil
        }

        if index + 1 == controllers.count {
            // 移除原来返回controllers.first的无限轮播逻辑,触发预加载后返回nil
            parent.onLast()
            return nil
        }
        
        return controllers[index + 1]
    }

    func pageViewController(
        _ pageViewController: UIPageViewController,
        didFinishAnimating finished: Bool,
        previousViewControllers: [UIViewController],
        transitionCompleted completed: Bool) {
            // 转场结束后无论成功失败都解锁
            defer { isTransitioning = false }

            guard completed,
                  let visibleViewController = pageViewController.viewControllers?.first,
                  let index = controllers.firstIndex(of: visibleViewController) else {
                return
            }
            parent.currentPage = index

            // 滑到最后一页时,异步追加新控制器,不要在当前回调栈内同步修改数组
            if parent.direction == .vertical && index == controllers.count - 1 {
                DispatchQueue.main.async { [weak self, weak pageViewController] in
                    guard let self = self, let _ = pageViewController, !self.isTransitioning else { return }
                    let pendingItems = self.parent.MarketVModel.discover_queue_space_summaries
                    guard !pendingItems.isEmpty else { return }

                    // 批量创建新控制器后一次性追加
                    let newControllers = pendingItems.map { item in
                        UIHostingController(rootView: AnyView(DiscoverSpacePresentational(space: item)))
                    }
                    self.controllers.append(contentsOf: newControllers)
                    self.parent.MarketVModel.discover_queue_space_summaries.removeAll()
                    // 追加完成后不需要主动调用setViewControllers,下次滑动时UIPageViewController会自动从数据源读取新控制器
                }
            }
    }
}

2. 修改updateUIViewController逻辑,避免无效调用

只有当前显示页面和目标页面不一致、且不在转场过程中时,才调用setViewControllers。主动触发页面跳转时,使用动画完成后无动画重置的方案修复UIPageViewController内部状态不一致的已知问题。

func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) {
    // 转场中直接跳过,避免状态冲突
    guard !context.coordinator.isTransitioning else { return }
    // 当前已经显示目标页,无需操作
    if let currentVC = pageViewController.viewControllers?.first,
       let currentIndex = context.coordinator.controllers.firstIndex(of: currentVC),
       currentIndex == currentPage {
        return
    }
    
    let targetVC = context.coordinator.controllers[currentPage]
    // 主动跳页时使用动画+完成后无动画重置的方案
    pageViewController.setViewControllers([targetVC], direction: .forward, animated: true) { [weak pageViewController] finished in
        guard finished, let pageVC = pageViewController else { return }
        DispatchQueue.main.async {
            pageVC.setViewControllers([targetVC], direction: .forward, animated: false, completion: nil)
        }
    }
}

3. 修正PageView的Equatable实现

不要永远返回true,只在核心属性一致时才跳过刷新,避免SwiftUI无效刷新触发多余的updateUIViewController调用:

static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool {
    return lhs.currentPage == rhs.currentPage && lhs.direction == rhs.direction
}

额外说明

  • 后台数据拉取、数据处理逻辑本身不会导致UIPageViewController异常,问题只出在「转场过程中修改UI相关状态」这一个点上,数据处理放在后台线程执行完全不会影响交互
  • 追加新控制器时不需要主动调用setViewControllers,只要把控制器加入controllers数组,UIPageViewController下次触发滑动切页时会自动通过数据源方法拿到新的控制器,不会打断当前交互
  • 不要用DispatchQueue.main.async包裹所有setViewControllers调用,只需要在主动跳页的完成回调里用一次即可,滥用反而会导致首次加载时序错乱崩溃

内容的提问来源于stack exchange,提问作者Heron. F

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 00:16:04