SwiftUI UIPageViewController追加控制器时崩溃与滑动故障修复
问题根因
你碰到的双滑动异常、_flushViewController崩溃本质是同一个问题:UIPageViewController内部有非常严格的状态机限制,转场动画执行期间绝对不能调用setViewControllers,也不能修改其数据源读取的控制器数组。你的现有实现存在3个核心冲突:
- 同时实现了无限轮播逻辑(滑到最后一页返回第一页控制器)和滑动加载更多逻辑,两个逻辑直接冲突,导致滑动到边界时UIPageViewController同时收到「回到第一页」和「追加新页」两个指令,出现来回滑动的异常
updateUIViewController会在每次SwiftUI刷新时无脑调用setViewControllers,快速滑动时转场还没结束就触发重复设置,直接触发内部断言崩溃- 在转场结束的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
相关产品推荐
相关产品推荐

