How to tell SwiftUI views to bind to nested ObservableObjects

ghz 1years ago ⋅ 2227 views

Question

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.


Answer

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()
}