PageViewController如何使用现有页面替代初始化控制器实现视图更新
问题描述
参考苹果官方SwiftUI与UIKit交互教程实现了PageViewController组件,当前实现中,页面在Coordinator的初始化方法中通过如下代码转换为控制器存入数组:
controllers = parent.pages.map { UIHostingController(rootView: $0) }
当SwiftUI视图中的页面内容发生更新时,用户无法看到更新效果,推测原因是Coordinator的init方法不会在视图更新时重新触发,已初始化的controllers数组无法同步最新的pages数据。需要实现直接使用最新的pages数据,而非固定使用初始化生成的controllers数组,保证页面正常更新。
现有代码
PageViewController.swift
import SwiftUI import UIKit struct PageViewController<Page: View>: UIViewControllerRepresentable { var pages: [Page] var onLast: () -> AnyView @Binding var currentPage: Int func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIPageViewController { let pageViewController = UIPageViewController( transitionStyle: .scroll, navigationOrientation: .vertical) pageViewController.dataSource = context.coordinator pageViewController.delegate = context.coordinator return pageViewController } func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { pageViewController.setViewControllers( [context.coordinator.controllers[currentPage]], direction: .forward, animated: true) } class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate { var parent: PageViewController var controllers = [UIViewController]() init(_ pageViewController: PageViewController) { parent = pageViewController controllers = parent.pages.map { UIHostingController(rootView: $0) } } func pageViewController( _ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return controllers.last } 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 { return controllers.first } return controllers[index + 1] } func pageViewController( _ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed, let visibleViewController = pageViewController.viewControllers?.first, let index = controllers.firstIndex(of: visibleViewController) { if index + 1 == controllers.count { print("index: \(index)") print(controllers.count) controllers.append(UIHostingController(rootView: parent.onLast())) } parent.currentPage = index } } } } struct PageView<Page: View>: View, Equatable { static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool { return true } var pages: [Page] var onLast: () -> AnyView @Binding var currentPage: Int var body: some View { PageViewController(pages: pages, onLast: onLast, currentPage: $currentPage) } }
测试用SwiftUI视图
import SwiftUI struct Test: View { @State var color: Color = Color.blue var body: some View { PageView( pages: [ AnyView( color ), AnyView( Color.purple ), AnyView(Color.green), AnyView(Color.orange), ], onLast: { color = Color.red }, currentPage: $currentPage ) .equatable() } }
解决方案
问题根源有两个:
PageView的==实现直接返回true,会导致SwiftUI判定视图永远不需要更新,updateUIViewController生命周期方法根本不会被触发- Coordinator作为持久存在的实例,只会初始化一次,内部缓存的
controllers数组、持有的parent引用都不会自动同步最新的视图状态,pages更新时没有刷新对应UIHostingController的根视图,也没有处理页数变化的场景
修复步骤
- 修正
PageView的Equatable实现,禁止无脑返回true,仅在真正无状态变化时返回相等 - 在
updateUIViewController方法中同步最新的parent引用到Coordinator,按索引更新已有UIHostingController的根视图,同步pages的增减变化,保留滚动到末尾时动态追加的控制器 - 切换显示页面时增加判断,仅当当前显示控制器和目标控制器不一致时才触发转场,避免无意义的动画
修改后的核心代码:
import SwiftUI import UIKit struct PageViewController<Page: View>: UIViewControllerRepresentable { var pages: [Page] var onLast: () -> AnyView @Binding var currentPage: Int func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> UIPageViewController { let pageViewController = UIPageViewController( transitionStyle: .scroll, navigationOrientation: .vertical) pageViewController.dataSource = context.coordinator pageViewController.delegate = context.coordinator // 初始化时默认显示第一页 if let first = context.coordinator.controllers.first { pageViewController.setViewControllers([first], direction: .forward, animated: false) } return pageViewController } func updateUIViewController(_ pageViewController: UIPageViewController, context: Context) { // 同步最新的parent引用到coordinator context.coordinator.parent = self // 按索引同步最新的pages到controllers数组 var newControllers: [UIViewController] = [] for (index, page) in pages.enumerated() { if index < context.coordinator.controllers.count, let host = context.coordinator.controllers[index] as? UIHostingController<Page> { // 更新已有控制器的根视图,不新建实例 host.rootView = page newControllers.append(host) } else { // 对应位置不存在控制器时新建 newControllers.append(UIHostingController(rootView: page)) } } // 保留滚动到末尾时动态追加的控制器 if context.coordinator.controllers.count > pages.count { newControllers.append(contentsOf: Array(context.coordinator.controllers[pages.count...])) } context.coordinator.controllers = newControllers // 计算当前显示的控制器索引,判断转场方向 let targetController = context.coordinator.controllers[currentPage] guard pageViewController.viewControllers?.first != targetController else { return } let currentIndex = pageViewController.viewControllers?.first.flatMap { context.coordinator.controllers.firstIndex(of: $0) } ?? 0 pageViewController.setViewControllers( [targetController], direction: currentPage > currentIndex ? .forward : .reverse, animated: true ) } class Coordinator: NSObject, UIPageViewControllerDataSource, UIPageViewControllerDelegate { var parent: PageViewController var controllers = [UIViewController]() init(_ pageViewController: PageViewController) { self.parent = pageViewController self.controllers = parent.pages.map { UIHostingController(rootView: $0) } } func pageViewController( _ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { guard let index = controllers.firstIndex(of: viewController) else { return nil } if index == 0 { return controllers.last } 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 { return controllers.first } return controllers[index + 1] } func pageViewController( _ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { if completed, let visibleViewController = pageViewController.viewControllers?.first, let index = controllers.firstIndex(of: visibleViewController) { if index + 1 == controllers.count { controllers.append(UIHostingController(rootView: parent.onLast())) } // 避免越界 parent.currentPage = min(index, parent.pages.count - 1) } } } } struct PageView<Page: View>: View, Equatable { static func == (lhs: PageView<Page>, rhs: PageView<Page>) -> Bool { // 仅当页码、页数一致时判定视图相等 lhs.currentPage == rhs.currentPage && lhs.pages.count == rhs.pages.count } var pages: [Page] var onLast: () -> AnyView @Binding var currentPage: Int var body: some View { PageViewController(pages: pages, onLast: onLast, currentPage: $currentPage) } }
测试代码需要补上缺失的currentPage状态定义即可正常运行:
struct Test: View { @State var color: Color = Color.blue @State var currentPage: Int = 0 // 补上缺失的状态定义 var body: some View { PageView( pages: [ AnyView(color), AnyView(Color.purple), AnyView(Color.green), AnyView(Color.orange), ], onLast: { color = Color.red return AnyView(Color.red) // onLast需要返回View,原代码缺失返回值 }, currentPage: $currentPage ) .equatable() } }
修复后滑动到最后一页时,第一页的颜色会正常更新为红色,所有pages的状态变化都会同步到页面显示。
内容的提问来源于stack exchange,提问作者Heron. F
相关产品推荐
相关产品推荐

