SwiftUI MVVM:父视图更新时重新初始化子视图模型

Posted

技术标签:

【中文标题】SwiftUI MVVM:父视图更新时重新初始化子视图模型【英文标题】:SwiftUI MVVM: child view model re-initialized when parent view updated 【发布时间】:2020-04-07 05:38:10 【问题描述】:

我正在尝试在 SwiftUI 应用程序中使用 MVVM,但似乎每当父子视图都观察到 ObservableObject 时,子视图的视图模型(例如 NavigationLink 中的视图模型)都会重新初始化已更新。这会导致孩子的本地状态被重置,网络数据被重新加载等。

我猜这是因为这会导致父级的 body 被重新评估,其中包含 SubView 的视图模型的构造函数,但我无法找到让我创建视图模型的替代方法不要活在视野之外。我需要能够将数据从父视图模型传递给子视图模型。

这是我们正在尝试完成的一个非常简化的操场,其中递增 EnvCounter.counter 重置 SubView.counter

import SwiftUI
import PlaygroundSupport

class EnvCounter: ObservableObject 
    @Published var counter = 0


struct ContentView: View 
    @ObservedObject var envCounter = EnvCounter()

    var body: some View 
        VStack 
            Text("Parent view")
            Button(action:  self.envCounter.counter += 1 ) 
                Text("EnvCounter is at \(self.envCounter.counter)")
            
            .padding(.bottom, 40)

            SubView(viewModel: .init())
        
        .environmentObject(envCounter)
    


struct SubView: View 
    class ViewModel: ObservableObject 
        @Published var counter = 0
    

    @EnvironmentObject var envCounter: EnvCounter
    @ObservedObject var viewModel: ViewModel

    var body: some View 
        VStack 
            Text("Sub view")

            Button(action:  self.viewModel.counter += 1 ) 
                Text("SubView counter is at \(self.viewModel.counter)")
            

            Button(action:  self.envCounter.counter += 1 ) 
                Text("EnvCounter is at \(self.envCounter.counter)")
            
        
    


PlaygroundPage.current.setLiveView(ContentView())

【问题讨论】:

这是正常行为。刷新时View.body 可计算属性被调用,所以里面的任何代码,没有被内部状态条件隐藏,都会被执行,所以所有可见的视图构造函数都会被调用。只是不要在视图构造函数和/或属性默认值中添加任何繁重的内容,将所有这些逻辑移到视图之外(会有好处 - 快速 UI 渲染)。 【参考方案1】:

在 Xcode 12 的 SwiftUI 中添加了一个新的属性包装器,@StateObject。您应该能够通过简单地将@ObservedObject 更改为@StateObject 来修复它,如下所示。

struct SubView: View 
    class ViewModel: ObservableObject 
        @Published var counter = 0
    

    @EnvironmentObject var envCounter: EnvCounter
    @StateObject var viewModel: ViewModel // change on this line

    var body: some View 
        // ...
    

【讨论】:

直接正确的答案。【参考方案2】:

为了解决这个问题,我创建了一个名为 ViewModelProvider 的自定义帮助器类。

提供者为您的视图获取一个哈希值,以及一个构建 ViewModel 的方法。然后它要么返回 ViewModel,要么在它第一次收到该哈希时构建它。

只要您确保哈希值保持不变,只要您想要相同的 ViewModel,就可以解决问题。

class ViewModelProvider 
    private static var viewModelStore = [String:Any]()
    
    static func makeViewModel<VM>(forHash hash: String, usingBuilder builder: () -> VM) -> VM 
        if let vm = viewModelStore[hash] as? VM 
            return vm
         else 
            let vm = builder()
            viewModelStore[hash] = vm
            return vm
        
    

然后在你的 View 中,你可以使用 ViewModel:

Struct MyView: View 
    @ObservedObject var viewModel: MyViewModel
    
    public init(thisParameterDoesntChangeVM: String, thisParameterChangesVM: String) 
        self.viewModel = ViewModelProvider.makeViewModel(forHash: thisParameterChangesVM) 
            MOFOnboardingFlowViewModel(
                pages: pages,
                baseStyleConfig: style,
                buttonConfig: buttonConfig,
                onFinish: onFinish
            )
        
    

在这个例子中,有两个参数。哈希中只使用了thisParameterChangesVM。这意味着即使thisParameterDoesntChangeVM 发生变化并且视图被重建,视图模型保持不变。

【讨论】:

我走了同样的路,但不是 [String:Any] 使用 NSMapTable(keyOptions: NSPointerFunctions.Options.strongMemory, valueOptions: NSPointerFunctions.Options.weakMemory) 在视图上删除 viewModel消失。【参考方案3】:

我遇到了同样的问题,你的猜测是正确的,SwiftUI 每次它的状态改变时都会计算你所有的父体。解决方案是将子 ViewModel init 移动到父 ViewModel,这是您示例中的代码:

class EnvCounter: ObservableObject 
    @Published var counter = 0
    @Published var subViewViewModel = SubView.ViewModel.init()


struct CounterView: View 
    @ObservedObject var envCounter = EnvCounter()

    var body: some View 
        VStack 
            Text("Parent view")
            Button(action:  self.envCounter.counter += 1 ) 
                Text("EnvCounter is at \(self.envCounter.counter)")
            
            .padding(.bottom, 40)

            SubView(viewModel: envCounter.subViewViewModel)
        
        .environmentObject(envCounter)
    

【讨论】:

以上是关于SwiftUI MVVM:父视图更新时重新初始化子视图模型的主要内容,如果未能解决你的问题,请参考以下文章

SwiftUI 和 MVVM - 模型和视图模型之间的通信

如何在 SwiftUI 中实现 MVVM 模式?视图不会重新渲染

Angular6 - 初始化父组件时如何初始化子组件?

创建父文档时未初始化子文档。 Nestjs/猫鼬

SwiftUI - 更新父视图的状态时如何保留子视图的状态?

SwiftUI 绑定到父视图重新渲染子视图