繁体   English   中英

SwiftUI MVVM:子视图 model 在父视图更新时重新初始化

[英]SwiftUI MVVM: child view model re-initialized when parent view updated

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

我猜这是因为这会导致重新评估父母的body ,其中包含SubView的视图 model 的构造函数,但我无法找到一个替代方案,让我创建不会超越生命的视图模型的看法。 我需要能够将数据从父视图传递给子视图 model。

这是我们试图完成的一个非常简化的操场,其中递增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())

在 Xcode 12, @StateObject的 SwiftUI 中添加了一个新的属性包装器。 您应该能够通过简单地更改@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 {
        // ...
    }
}

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

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

只要您确保 hash 保持不变,只要您想要相同的 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
        }
    }
}

然后在您的视图中,您可以使用 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中仅使用了 thisParameterChangesVM。 这意味着即使thisParameterDoesntChangeVM发生变化并且视图被重建,视图 model 保持不变。

我遇到了同样的问题,您的猜测是正确的,SwiftUI 每次 state 更改时都会计算您的所有父体。 解决方案是将子 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)
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM