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

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()
    }
}

解决方案

问题根源有两个:

  1. PageView的==实现直接返回true,会导致SwiftUI判定视图永远不需要更新,updateUIViewController生命周期方法根本不会被触发
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 14:31:14