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我遇到了同样的问题,你的猜测是正确的,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 模式?视图不会重新渲染