So I'm starting to work with @Observable macros instead of the old ObservedObject, and there's something I can't find an answer to: should an update of an observable object's (@Observable) property nested inside an observable object (@Observable) referenced in a SwiftUI view trigger an update of the view if the nested prop is referenced in the view ?
Here is a code sample of what I'm talking about:
import SwiftUI
@main
struct MyApp: App {
var model: MainModel = MainModel()
var body: some Scene {
WindowGroup {
NestedObservableView(model: model)
}
}
}
@Observable class MainModel {
var value: String = "main model value"
var nestedModel: NestedModel = NestedModel()
func updateValue() {
value = randomStrings.randomElement()!
}
}
@Observable class NestedModel {
var value: String = "nested model value"
func updateValue() {
value = randomStrings.randomElement()!
}
}
struct NestedObservableView: View {
var model: MainModel
var body: some View {
VStack {
Text(model.value)
Button {
model.updateValue()
} label: {
Text("update first model value")
}
Text(model.nestedModel.value)
Button {
model.nestedModel.updateValue()
} label: {
Text("update nested model value")
}
}
}
}
let randomStrings: [String] = [
"Fierce Tiger",
"Lazy Panda",
"Cunning Fox",
"Gentle Dolphin",
"Wild Horse",
"Shy Rabbit",
"Brave Eagle",
"Noisy Parrot",
"Playful Otter",
"Lonely Wolf",
"Slow Turtle",
"Sneaky Weasel",
"Happy Penguin",
"Angry Gorilla",
"Tiny Mouse",
"Loyal Dog",
"Curious Cat",
"Hungry Bear",
"Fast Cheetah",
"Sleepy Koala"
]
#Preview {
let model = MainModel()
NestedObservableView(model: model)
}
The question here is to know if the tap on the "update nested model value" button should trigger an update of Text(model.nestedModel.value)
?
I didn't find anything in the official documentation that talks about nesting @Observable, and no reference in the WWDC23: Discover Observation in SwiftUI talk either, so my guess was it shouldn't update. I asked some AI models (GPT-4o, Claude Sonnet 4) and their answer was that it shouldn't work, unless using @Bindable or some manual propagation.
That said, when testing with this sample code, the label does update on the tap of the button. I don't understand if this code is an edge case, and if so why exactly is the UI updating? Or if it should update, and if so why can't I find it in the doc?
Yes any property of @Observable
that has its getter called inside body
will have body
called again when the setter is called. Essentially body
is called inside a withObservationTracking { }
. You should be aware there is some debate in the community around a memory leak when the getter is called and then the view is removed because it will wait forever on the setter. The theory is it's a design flaw because the @Observable
has an internal registrar object that essentially observes itself, creating a retain cycle.
Since your nested model is not managing relationships and not doing any loading or saving it would be better as a struct, e.g.
fileprivate let model = MainModel() // lazy
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
NestedObservableView(model: model)
}
}
}
@Observable class MainModel {
var value: String = "main model value"
var nestedModel = NestedModel()
func updateValue() {
value = randomStrings.randomElement()!
}
}
struct NestedModel {
var value: String = "nested model value"
mutating func updateValue() {
value = randomStrings.randomElement()!
}
}