@StateObject vs @ObservedObject 当外部传递但由视图拥有时

@StateObject vs @ObservedObject when passed externally but owned by the view

基于这个答案:

Apple 文档代码在这里:https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app

在 SwiftUI 应用程序中,当 View 实例化对象本身时,应使用 @StateObject 属性 包装器,以便在视图更新期间不会重新创建对象。

如果对象在其他地方实例化,则应使用 @ObservedObject 包装器。

但是,有一条细线让它有点不清楚:如果对象在其他地方实例化,但“注入”到 View 然后视图是唯一所有者/持有者怎么办那个物体?应该是 @StateObject 还是 @ObservedObject?

说明要点的示例代码:

import SwiftUI
import Combine
import Foundation


struct ViewFactory {
    func makeView() -> some View {
        let viewModel = ViewModel()
        return NameView(viewModel)
    }
}


final class ViewModel: ObservableObject {
    @Published var name = ""
    init() {}
}


struct NameView: View {

    // Should this be an `@ObservedObject` or `@StateObject`?
    @ObservedObject var viewModel: ViewModel

    init(_ viewModel: ViewModel) {
        self.viewModel = viewModel
    }

    var body: some View {
        Text(viewModel.name)
    }
}

基于本文:https://www.hackingwithswift.com/quick-start/swiftui/whats-the-difference-between-observedobject-state-and-environmentobject

@StateObject 和@ObservedObject 之间有一个重要的区别,那就是所有权——哪个视图创建了对象,哪个视图只是在监视它。

规则是这样的:无论哪个视图首先创建您的对象,都必须使用@StateObject,以告诉 SwiftUI 它是数据的所有者并负责保持数据的存活。所有其他视图必须使用@ObservedObject,以告诉 SwiftUI 他们想要观察对象的变化但不直接拥有它。

看来如果View要实例化ViewModel,就必须用@StateObject声明。我的代码非常相似,唯一的区别是 ViewModel 是在别处创建的,但是 View 在初始化后“拥有”它。

这是一个非常有趣的问题。这里发生了一些微妙的行为。

首先,请注意您不能在 NameView 中将 @ObservedObject 更改为 @StateObject。它不会编译:

struct NameView: View {
    @StateObject var viewModel: ViewModel

    init(_ viewModel: ViewModel) {
        self.viewModel = viewModel
        //   ^  Cannot assign to property: 'viewModel' is a get-only property
    }
    ...
}

要使其编译,您必须初始化底层 _viewModel 存储的 属性 类型 StateObject<ViewModel>:

struct NameView: View {
    @StateObject var viewModel: ViewModel

    init(_ viewModel: ViewModel) {
        _viewModel = .init(wrappedValue: viewModel)
    }
    ...
}

但那里隐藏着一些东西。 StateObject.init(wrappedValue:) 声明如下:

public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)

因此作为参数给出的表达式(只是上面的 viewModel)被包裹在一个闭包中,并且 不是 立即计算。该闭包被存储以备后用,这就是为什么它是 @escaping.

正如您可能猜到的那样,我们必须跳过这些步骤才能使其编译,这是一种奇怪的使用方式 StateObject。正常使用是这样的:

struct NormalView: View {
    @StateObject var viewModel = ViewModel()

  var body: some View {
        Text(viewModel.name)
    }
}

并且 以这种奇怪的方式进行操作有一些缺点。 要了解这些缺点,我们需要查看 makeView()NormalView() 的上下文被评估。假设它看起来像这样:

struct ContentView: View {
    @Binding var count: Int

    var body: some View {
        VStack {
            Text("count: \(count)")
            NormalView()
            ViewFactory().makeView()
        }
    }
}

count 的值发生变化时,SwiftUI 将再次向 ContentView 询问其 body,这将再次评估 NormalView()makeView()

因此 body 在第二次计算期间调用 NormalView(),这会创建 NormalView 的另一个实例。 NormalView.init 创建一个调用 ViewModel() 的闭包,并将闭包传递给 StateObject.init(wrappedValue:)。但是 StateObject.init 不会 立即计算这个闭包。它将它存储起来供以后使用。

然后 body 调用 makeView() 立即调用 ViewModel()。它将新的 ViewModel 传递给 NameView.init,后者将新的 ViewModel 包装在闭包中并将闭包传递给 StateObject.init(wrappedValue:)。此 StateObject 也不会立即评估闭包,但无论如何都会创建新的 ViewModel

ContentView.body returns 之后的某个时间,SwiftUI 想要调用 NormalView.body。但在这样做之前,它必须确保 NormalView 中的 StateObject 有一个 ViewModel。它注意到此 NormalView 正在替换视图层次结构中相同位置的先前 NormalView,因此它检索先前 NormalView 使用的 ViewModel 并将其放入StateObject 的新 NormalView。它不会执行给StateObject.init的闭包,所以它不会创建新的ViewModel

再后来,SwiftUI要调用NameView.body。但在这样做之前,它必须确保这个 NameView 中的 StateObject 有一个 ViewModel。它注意到此 NameView 正在替换视图层次结构中相同位置的先前 NameView,因此它检索先前 NameView 使用的 ViewModel 并将其放入StateObject 的新 NameView。它执行给StateObject.init的闭包,因此它不使用闭包引用的ViewModel .但是 ViewModel 还是创建了。

因此,您使用 @StateObject 的怪异方式有两个缺点:

  1. 您每次调用 makeView 时都会创建一个新的 ViewModel,即使那个 ViewModel 可能永远不会被使用。这可能很昂贵,具体取决于您的 ViewModel.
  2. 您正在创建 ViewModel,而 ContentView.body getter 是 运行。如果创建 ViewModel 有副作用,这可能会混淆 SwiftUI。 SwiftUI 期望 body getter 是一个纯函数。在 NormalView 的情况下,SwiftUI 在已知时间调用 StateObject 的闭包,此时它可能会更好地准备处理副作用。

所以,回到你原来的问题:

Should it be @StateObject or @ObservedObject?

好吧,哈哈,如果没有看到一个不像玩具的例子,很难回答这个问题。但是如果你确实需要使用 @StateObject,你应该尝试以“正常”方式初始化它。