When exactly does ObservableObject publish, and why do nested objects and equal assignments surprise people?

8.10.4 · L3 · Interview

Predict first

What do the sinks see?

import Combine

final class Child: ObservableObject { @Published var n = 0 }
final class Parent: ObservableObject {
    @Published var title = "t"
    @Published var items: [Int] = []
    let child = Child()
}
let p = Parent()
var seen: [String] = []
let willChange = p.objectWillChange.sink { seen.append("will change, title is still \(p.title)") }
p.title = "u"
print(seen); seen = []
p.child.n = 5
print("nested object change reaches the parent:", !seen.isEmpty)
p.items = [1]; seen = []
p.items = [1]
print("assigning an equal array fires:", seen.count)
willChange.cancel()
let projected = p.$title.sink { v in seen.append("$title sent \(v) while title is \(p.title)") }
seen = []
p.title = "v"
print(seen)
projected.cancel()
  • willChange sees the old title / nested change does not reach the parent / equal array fires once / $title sends the new value while title is still old
  • willChange sees the new title
  • the nested change reaches the parent

Mental model. objectWillChange is a “wet paint” sign hung before the painting starts: everyone is told early, and anyone who looks right now still sees the old colour.

@Published sends objectWillChange before the new value is stored. In the check the sink runs while title is still t, and the $title publisher likewise delivers the new value while the property still holds the old one. That is why code reacting inside a sink must use the value it is given (or hop to the next run-loop turn), not re-read the object. SwiftUI is fine with this because it re-renders later, after the change has landed.

Two surprises. Nesting does not propagate: the parent only publishes for its own @Published properties, so a change inside child never reaches an @ObservedObject parent (the check prints false); forward it by subscribing to the child’s objectWillChange or, better, move to @Observable. And there is no equality check: assigning an equal array still publishes, so every observer re-evaluates. Compare before assigning when updates are frequent. Mutating an array element in place publishes once per mutation, so batch edits into a local copy. All of this is why @Observable is cheaper by default, while ObservableObject stays for pre-iOS-17 targets and when you want a Combine publisher.

@Published announces before the value changes and has no equality check.

Common traps

  • Reading the object inside the objectWillChange sink. It still has the old value.
  • Nesting ObservableObjects and expecting the outer view to update. It does not.

They might follow up with

  • How do you forward a child’s changes? Subscribe to its objectWillChange and send your own.
  • Is a struct in @Published cheaper? Any mutation publishes the whole property.

Related questions

  • 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
  • 8.10.3 What exactly does Observation track: computed properties, ignored properties, collections, nested objects and…
  • 8.2.1 How does SwiftUI decide to re-run body, and how do you keep updates cheap?

← What exactly does Observation track: computed properties, ignored…   If a parent changes one environment value, which child views re-run… →