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

时间:2020-04-07 05:38:10

标签: ios swift swiftui

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

我猜是因为这会导致对父级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())

3 个答案:

答案 0 :(得分:4)

为解决此问题,我创建了一个名为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
        }
    }
}

然后在您的视图中,可以使用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发生变化并重建了视图,视图模型也保持不变。

答案 1 :(得分:2)

在Xcode 12 @StateObject中,新的属性包装器被添加到SwiftUI。您的问题应该得到解决。

答案 2 :(得分:0)

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