为什么我的 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 中进行以下更改,我会得到我想要的行为。

据我了解,ChildViewviewModel@ObservedObject 是不合适的,因为 ChildView 拥有 viewModel@ObservedObject 给了我我需要的行为,而 @StateObject 没有。

以下是我关注的差异:

@ObservedObject 实际上是正确的吗,因为 ViewModel 包含 @BindingParentView 中创建的 @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)
    }
}