为什么我的 SwiftUI 视图无法从 @StateObject 的 @Binding 成员获取 onChange 更新?
Why does my SwiftUI view not get onChange updates from a @Binding member of a @StateObject?
鉴于我在下面概述的设置,我正在尝试确定为什么 ChildView
的 .onChange(of: _)
没有收到更新。
import SwiftUI
struct SomeItem: Equatable {
var doubleValue: Double
}
struct ParentView: View {
@State
private var someItem = SomeItem(doubleValue: 45)
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { someItem.doubleValue += 10.0 }
.overlay { ChildView(someItem: $someItem) }
}
}
struct ChildView: View {
@StateObject
var viewModel: ViewModel
init(someItem: Binding<SomeItem>) {
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.someItem) { _ in
print("Change Detected", viewModel.someItem.doubleValue)
}
}
}
@MainActor
final class ViewModel: ObservableObject {
@Binding
var someItem: SomeItem
public init(someItem: Binding<SomeItem>) {
self._someItem = someItem
}
public func changeItem() {
self.someItem = SomeItem(doubleValue: .zero)
}
}
有趣的是,如果我在 ChildView
中进行以下更改,我会得到我想要的行为。
将@StateObject
改为@ObservedObject
将_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
更改为viewModel = ViewModel(someItem: someItem)
据我了解,ChildView
的 viewModel
是 @ObservedObject
是不合适的,因为 ChildView
拥有 viewModel
但 @ObservedObject
给了我我需要的行为,而 @StateObject
没有。
以下是我关注的差异:
- 使用
@ObservedObject
时,我可以点击黑色区域并查看应用于白色文本和红色矩形的更改。我还可以点击红色矩形并通过白色文本查看 ParentView
中观察到的变化。
- 使用
@StateObject
时,我可以点击黑色区域并查看应用于白色文本和红色矩形的更改。问题在于我可以点击此处的红色矩形并查看 ParentView
中反映的更改,但 ChildView
无法识别更改(旋转不会更改并且不会打印“检测到更改”)。
@ObservedObject
实际上是正确的吗,因为 ViewModel
包含 @Binding
到 ParentView
中创建的 @State
?
实际上我们在 SwiftUI 中根本不使用视图模型对象,请参阅 [Data Essentials in SwiftUI WWDC 2020]。如 4:33 处的视频所示,创建一个自定义结构来保存项目,例如ChildViewConfig
并在父级的 @State
中初始化它。在处理程序中设置 childViewConfig.item
或添加任何变异的自定义函数。如果您需要写入权限,请将绑定 $childViewConfig
或 $childViewConfig.item
传递给子视图。如果你坚持结构和值语义,一切都非常简单。
通常情况下,我不会写出如此复杂的问题解决方案,但从您对另一个答案的评论来看,您似乎需要遵守某些架构问题。
您的初始方法的一般问题是 onChange
仅在视图触发渲染时才会 运行。通常,发生这种情况是因为 passed-in 属性 发生了变化,@State
发生了变化,或者 ObservableObject
上的发布者发生了变化。在这种情况下,其中 none 是真实的——您的 ObservableObject
上有一个 Binding
,但没有任何东西会触发 re-render 的视图。如果 Bindings
提供了一个发布者,很容易挂钩到那个值,但由于他们没有,看起来合乎逻辑的方法是以我们可以观看的方式将状态存储在父视图中@Published
值。
同样,这不一定是我要走的路线,但希望它符合您的要求:
struct SomeItem: Equatable {
var doubleValue: Double
}
class Store : ObservableObject {
@Published var someItem = SomeItem(doubleValue: 45)
}
struct ParentView: View {
@StateObject private var store = Store()
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(store.someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { store.someItem.doubleValue += 10.0 }
.overlay { ChildView(store: store) }
}
}
struct ChildView: View {
@StateObject private var viewModel: ViewModel
init(store: Store) {
_viewModel = StateObject(wrappedValue: ViewModel(store: store))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.store.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.store.someItem.doubleValue) { _ in
print("Change Detected", viewModel.store.someItem.doubleValue)
}
}
}
@MainActor
final class ViewModel: ObservableObject {
var store: Store
var cancellable : AnyCancellable?
public init(store: Store) {
self.store = store
cancellable = store.$someItem.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
public func changeItem() {
store.someItem = SomeItem(doubleValue: .zero)
}
}
鉴于我在下面概述的设置,我正在尝试确定为什么 ChildView
的 .onChange(of: _)
没有收到更新。
import SwiftUI
struct SomeItem: Equatable {
var doubleValue: Double
}
struct ParentView: View {
@State
private var someItem = SomeItem(doubleValue: 45)
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { someItem.doubleValue += 10.0 }
.overlay { ChildView(someItem: $someItem) }
}
}
struct ChildView: View {
@StateObject
var viewModel: ViewModel
init(someItem: Binding<SomeItem>) {
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.someItem) { _ in
print("Change Detected", viewModel.someItem.doubleValue)
}
}
}
@MainActor
final class ViewModel: ObservableObject {
@Binding
var someItem: SomeItem
public init(someItem: Binding<SomeItem>) {
self._someItem = someItem
}
public func changeItem() {
self.someItem = SomeItem(doubleValue: .zero)
}
}
有趣的是,如果我在 ChildView
中进行以下更改,我会得到我想要的行为。
将
@StateObject
改为@ObservedObject
将
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
更改为viewModel = ViewModel(someItem: someItem)
据我了解,ChildView
的 viewModel
是 @ObservedObject
是不合适的,因为 ChildView
拥有 viewModel
但 @ObservedObject
给了我我需要的行为,而 @StateObject
没有。
以下是我关注的差异:
- 使用
@ObservedObject
时,我可以点击黑色区域并查看应用于白色文本和红色矩形的更改。我还可以点击红色矩形并通过白色文本查看ParentView
中观察到的变化。 - 使用
@StateObject
时,我可以点击黑色区域并查看应用于白色文本和红色矩形的更改。问题在于我可以点击此处的红色矩形并查看ParentView
中反映的更改,但ChildView
无法识别更改(旋转不会更改并且不会打印“检测到更改”)。
@ObservedObject
实际上是正确的吗,因为 ViewModel
包含 @Binding
到 ParentView
中创建的 @State
?
实际上我们在 SwiftUI 中根本不使用视图模型对象,请参阅 [Data Essentials in SwiftUI WWDC 2020]。如 4:33 处的视频所示,创建一个自定义结构来保存项目,例如ChildViewConfig
并在父级的 @State
中初始化它。在处理程序中设置 childViewConfig.item
或添加任何变异的自定义函数。如果您需要写入权限,请将绑定 $childViewConfig
或 $childViewConfig.item
传递给子视图。如果你坚持结构和值语义,一切都非常简单。
通常情况下,我不会写出如此复杂的问题解决方案,但从您对另一个答案的评论来看,您似乎需要遵守某些架构问题。
您的初始方法的一般问题是 onChange
仅在视图触发渲染时才会 运行。通常,发生这种情况是因为 passed-in 属性 发生了变化,@State
发生了变化,或者 ObservableObject
上的发布者发生了变化。在这种情况下,其中 none 是真实的——您的 ObservableObject
上有一个 Binding
,但没有任何东西会触发 re-render 的视图。如果 Bindings
提供了一个发布者,很容易挂钩到那个值,但由于他们没有,看起来合乎逻辑的方法是以我们可以观看的方式将状态存储在父视图中@Published
值。
同样,这不一定是我要走的路线,但希望它符合您的要求:
struct SomeItem: Equatable {
var doubleValue: Double
}
class Store : ObservableObject {
@Published var someItem = SomeItem(doubleValue: 45)
}
struct ParentView: View {
@StateObject private var store = Store()
var body: some View {
Color.black
.overlay(alignment: .top) {
Text(store.someItem.doubleValue.description)
.font(.system(size: 50))
.foregroundColor(.white)
}
.onTapGesture { store.someItem.doubleValue += 10.0 }
.overlay { ChildView(store: store) }
}
}
struct ChildView: View {
@StateObject private var viewModel: ViewModel
init(store: Store) {
_viewModel = StateObject(wrappedValue: ViewModel(store: store))
}
var body: some View {
Rectangle()
.fill(Color.red)
.frame(width: 50, height: 70, alignment: .center)
.rotationEffect(
Angle(degrees: viewModel.store.someItem.doubleValue)
)
.onTapGesture { viewModel.changeItem() }
.onChange(of: viewModel.store.someItem.doubleValue) { _ in
print("Change Detected", viewModel.store.someItem.doubleValue)
}
}
}
@MainActor
final class ViewModel: ObservableObject {
var store: Store
var cancellable : AnyCancellable?
public init(store: Store) {
self.store = store
cancellable = store.$someItem.sink { [weak self] _ in
self?.objectWillChange.send()
}
}
public func changeItem() {
store.someItem = SomeItem(doubleValue: .zero)
}
}