@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)
}
}
@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
的怪异方式有两个缺点:
- 您每次调用
makeView
时都会创建一个新的 ViewModel
,即使那个 ViewModel
可能永远不会被使用。这可能很昂贵,具体取决于您的 ViewModel
.
- 您正在创建
ViewModel
,而 ContentView.body
getter 是 运行。如果创建 ViewModel
有副作用,这可能会混淆 SwiftUI。 SwiftUI 期望 body
getter 是一个纯函数。在 NormalView
的情况下,SwiftUI 在已知时间调用 StateObject
的闭包,此时它可能会更好地准备处理副作用。
所以,回到你原来的问题:
Should it be @StateObject
or @ObservedObject
?
好吧,哈哈,如果没有看到一个不像玩具的例子,很难回答这个问题。但是如果你确实需要使用 @StateObject
,你应该尝试以“正常”方式初始化它。
基于这个答案:
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)
}
}
@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
的怪异方式有两个缺点:
- 您每次调用
makeView
时都会创建一个新的ViewModel
,即使那个ViewModel
可能永远不会被使用。这可能很昂贵,具体取决于您的ViewModel
. - 您正在创建
ViewModel
,而ContentView.body
getter 是 运行。如果创建ViewModel
有副作用,这可能会混淆 SwiftUI。 SwiftUI 期望body
getter 是一个纯函数。在NormalView
的情况下,SwiftUI 在已知时间调用StateObject
的闭包,此时它可能会更好地准备处理副作用。
所以,回到你原来的问题:
Should it be
@StateObject
or@ObservedObject
?
好吧,哈哈,如果没有看到一个不像玩具的例子,很难回答这个问题。但是如果你确实需要使用 @StateObject
,你应该尝试以“正常”方式初始化它。