I have a SwiftUI view that takes in an EnvironmentObject called appModel
. It then reads the value appModel.submodel.count
in its body
method. I expect this to bind my view to the property count
on submodel
so that it re-renders when the property updates, but this does not seem to happen.
Is this a bug? And if not, what is the idiomatic way to have views bind to nested properties of environment objects in SwiftUI?
Specifically, my model looks like this...
class Submodel: ObservableObject { @Published var count = 0 } class AppModel: ObservableObject { @Published var submodel: Submodel = Submodel() }
And my view looks like this...
struct ContentView: View { @EnvironmentObject var appModel: AppModel var body: some View { Text("Count: \(appModel.submodel.count)") .onTapGesture { self.appModel.submodel.count += 1 } } }
When I run the app and click on the label, the count
property does increase but the label does not update.
I can fix this by passing in appModel.submodel
as a property to ContentView
, but I'd like to avoid doing so if possible.
Nested models does not work yet in SwiftUI, but you could do something like this
class SubModel: ObservableObject { @Published var count = 0 } class AppModel: ObservableObject { @Published var submodel: SubModel = SubModel() var anyCancellable: AnyCancellable? = nil init() { anyCancellable = submodel.objectWillChange.sink { [weak self] (_) in self?.objectWillChange.send() } } }
Basically your AppModel
catches the event from SubModel
and send it further to the View
.
Edit:
If you do not need SubModel
to be class, then you could try something like this either:
struct SubModel{ var count = 0 } class AppModel: ObservableObject { @Published var submodel: SubModel = SubModel() }
Sorin Lica's solution can solve the problem but this will result in code smell when dealing with complicated views.
What seems to better advice is to look closely at your views, and revise them to make more, and more targeted views. Structure your views so that each view displays a single level of the object structure, matching views to the classes that conform to ObservableObject
. In the case above, you could make a view for displaying Submodel
(or even several views) that display's the property from it that you want show. Pass the property element to that view, and let it track the publisher chain for you.
struct SubView: View { @ObservableObject var submodel: Submodel var body: some View { Text("Count: \(submodel.count)") .onTapGesture { self.submodel.count += 1 } } } struct ContentView: View { @EnvironmentObject var appModel: AppModel var body: some View { SubView(submodel: appModel.submodel) } }
This pattern implies making more, smaller, and focused views, and lets the engine inside SwiftUI do the relevant tracking. Then you don't have to deal with the book keeping, and your views potentially get quite a bit simpler as well.
You can check for more detail in this post: https://rhonabwy.com/2021/02/13/nested-observable-objects-in-swiftui/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With