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

13

我试图在SwiftUI app中使用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())

这是正常行为。刷新View.body可计算属性会被调用,因此任何未被内部状态条件隐藏的代码都将被执行,从而调用所有可见视图构造函数。只需不要在视图构造函数和/或属性默认值中放置任何繁重的内容,将所有逻辑移出视图(将获得奖励-快速UI渲染)。 - Asperi
3个回答

15
在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 {
        // ...
    }
}

直截了当且正确的答案。 - Kenan Begić
点击此答案查看为什么使用StateObject而不是ObservedObject会改变SubView的重新渲染行为。 - zr0gravity7

5
为了解决这个问题,我创建了一个名为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
我走了同样的路,但是使用NSMapTable<NSString, AnyObject>(keyOptions: NSPointerFunctions.Options.strongMemory, valueOptions: NSPointerFunctions.Options.weakMemory)代替[String:Any],以便在视图消失时移除viewModel。 - Andrey Soloviev

3

我遇到了同样的问题,你的猜测是正确的,SwiftUI在每次状态改变时都会计算所有父级视图。解决方法是将子视图模型的初始化移到父视图模型中,这是你示例代码中的代码:

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)
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接