Given the setup I've outlined below, I'm trying to determine why ChildView
's .onChange(of: _)
is not receiving updates.
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)
}
}
Interestingly, if I make the following changes in ChildView
, I get the behavior I want.
Change
@StateObject
to@ObservedObject
Change
_viewModel = StateObject(wrappedValue: ViewModel(someItem: someItem))
toviewModel = ViewModel(someItem: someItem)
From what I understand, it is improper for ChildView
's viewModel
to be @ObservedObject
because ChildView
owns viewModel
but @ObservedObject
gives me the behavior I need whereas @StateObject
does not.
Here are the differences I'm paying attention to:
- When using
@ObservedObject
, I can tap the black area and see the changes applied to both the white text and red rectangle. I can also tap the red rectangle and see the changes observed inParentView
through the white text. - When using
@StateObject
, I can tap the black area and see the changes applied to both the white text and red rectangle. The problem lies in that I can tap the red rectangle here and see the changes reflected inParentView
butChildView
doesn't recognize the change (rotation does not change and "Change Detected" is not printed).
Is @ObservedObject
actually correct since ViewModel
contains a @Binding
to a @State
created in ParentView
?
CodePudding user response:
Actually we don't use view model objects at all in SwiftUI, see [Data Essentials in SwiftUI WWDC 2020]. As shown in the video at 4:33 create a custom struct to hold the item, e.g. ChildViewConfig
and init it in an @State
in the parent. Set the childViewConfig.item
in a handler or add any mutating custom funcs. Pass the binding $childViewConfig
or $childViewConfig.item
to the to the child View if you need write access. It's all very simple if you stick to structs and value semantics.
CodePudding user response:
Normally, I would not write such a convoluted solution to a problem, but it sounds like from your comments on another answer there are certain architectural issues that you are required to conform to.
The general issue with your initial approach is that onChange
is only going to run when the view has a render triggered. Generally, that happens because some a passed-in property has changed, @State
has changed, or a publisher on an
ObservableObjecthas changed. In this case, none of those are true -- you have a
Bindingon your
ObservableObject, but nothing that triggers the view to re-render. If
Bindingsprovided a publisher, it would be easy to hook into that value, but since they do not, it seems like the logical approach is to store the state in the parent view in a way in which we can watch a
@Published` value.
Again, this is not necessarily the route I would take, but hopefully it fits your requirements:
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)
}
}