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))
to viewModel = 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:
@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 in ParentView
through the white text.@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 in ParentView
but ChildView
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
?
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 ObservableObject
has changed. In this case, none of those are true -- you have a Binding
on your ObservableObject
, but nothing that triggers the view to re-render. If Bindings
provided 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)
}
}