为什么我的 SwiftUI 视图无法从 @StateObject 的 @Binding 成员获取 onChange 更新?

问题描述 投票:0回答:2

鉴于我在下面概述的设置,我正在尝试确定为什么

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
中创建的
@State
,所以
ParentView
实际上是正确的吗?

swift mvvm swiftui swift5 property-wrapper
2个回答
4
投票

通常,我不会为问题编写如此复杂的解决方案,但从您对另一个答案的评论来看,您需要遵守某些架构问题。

您的初始方法的一般问题是

onChange
仅在视图触发渲染时才会运行。一般来说,发生这种情况是因为某些传入属性已更改、
@State
已更改,或者
ObservableObject
上的发布者已更改。在这种情况下,这些都不是真的——你的
Binding
上有一个
ObservableObject
,但没有任何东西触发视图重新渲染。如果
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)
    }
}


0
投票

实际上,我们在 SwiftUI 中根本不使用视图模型对象,因为

View
结构层次结构就是视图模型,请参阅 [SwiftUI WWDC 2020 中的数据要点]。如视频 4:33 所示,创建一个自定义结构来保存该项目,例如
ChildViewConfig
并在父级的
@State
中初始化它。在处理程序中设置
childViewConfig.item
或添加任何可变的自定义函数。如果需要写访问权限,请将绑定
$childViewConfig
$childViewConfig.item
传递给子视图。如果您坚持结构和值语义,这一切都会非常简单。

© www.soinside.com 2019 - 2024. All rights reserved.