SwiftUI中UIViewRepresentable的Coordinator如何自动同步viewModel?
结论
不存在完全无需额外代码的「自动同步」方案,但有两种更符合SwiftUI设计规范的实现方式,可以避免手动赋值的麻烦:
方案1:移除Coordinator内部的ViewModel存储
你当前的Coordinator已经持有了SwiftUIWebView的parent引用,而parent的viewModel是@ObservedObject属性,外部传入新实例时会自动更新。你完全不需要在Coordinator内部单独存储一份ViewModel,所有需要访问ViewModel的场景直接通过parent.viewModel获取即可,天然就是最新值,无需任何同步操作。
同时需要修正你现有代码的一个严重问题:不要在UIViewRepresentable结构体中存储WKWebView作为属性,UIViewRepresentable是值类型,每次视图更新都会生成新的结构体实例,存储的WKWebView会被反复创建,导致状态丢失和内存泄漏,正确写法是在makeUIView中创建WKWebView。
修改后的代码示例:
struct SwiftUIWebView : UIViewRepresentable { @ObservedObject var viewModel: WebViewModel func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIView(context: Context) -> WKWebView { print("SwiftUIWebView MAKE") let webView = WKWebView() // 直接在makeUIView中设置KVO,无需Coordinator存viewModel context.coordinator.estimatedProgressObserver = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in guard let self = self else { return } // 直接访问当前结构体的viewModel,永远是最新实例 self.viewModel.progress = webView.estimatedProgress } if let url = URL(string: viewModel.link) { webView.load(URLRequest(url: url)) } return webView } func updateUIView(_ uiView: WKWebView, context: Context) { // 这里不需要再做ViewModel同步 // 可选:如果需要响应link变化自动加载新网页,可以在这里加判断 if uiView.url?.absoluteString != viewModel.link, let url = URL(string: viewModel.link) { uiView.load(URLRequest(url: url)) } } } // Coordinator简化后代码 class Coordinator: NSObject { var parent: SwiftUIWebView var estimatedProgressObserver: NSKeyValueObservation? init(_ parent: SwiftUIWebView) { print("Coordinator init") self.parent = parent super.init() } deinit { estimatedProgressObserver = nil } }
方案2:从根源避免生成新的ViewModel实例
你遇到的同步问题本质是每次redraw状态变化时,都在body内直接新建了WebViewModel实例,这不符合SwiftUI的状态管理规范。使用@StateObject持有ViewModel可以保证它只会初始化一次,不会因为视图重绘生成新实例,自然不存在同步问题。
修改后的WebContentViewTest代码示例:
struct WebContentViewTest : View { @Binding var redraw:Bool // 用@StateObject持有ViewModel,生命周期和当前View绑定,只会初始化一次 @StateObject private var webModel = WebViewModel(progress: 0.0, link: "https://www.google.com") var body: some View { let _ = print("WebContentViewTest body") NavigationView { ZStack(alignment: .topLeading) { if redraw { WebViewContainer(model: webModel) } VStack { Button(action: { redraw.toggle() // 如果需要切换网页,直接修改model的link属性即可,无需新建实例 // webModel.link = "https://www.apple.com" }) { Text("redraw") } Spacer() } } .navigationBarTitle("Test Modal", displayMode: .inline) } } }
补充说明
如果你的业务场景确实需要传入不同的ViewModel实例(比如复用Web容器加载不同来源的网页),你当前使用的在updateUIView中手动赋值context.coordinator.viewModel = viewModel的写法,就是Apple官方推荐的标准实现,性能开销可以忽略,完全不需要刻意规避。
内容的提问来源于stack exchange,提问作者zumzum

