Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to tell SwiftUI views to bind to nested ObservableObjects

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.

like image 943
rjkaplan Avatar asked Oct 16 '19 05:10

rjkaplan


2 Answers

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() } 
like image 191
Sorin Lica Avatar answered Sep 21 '22 18:09

Sorin Lica


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/

like image 45
Hien Pham Avatar answered Sep 23 '22 18:09

Hien Pham