Answer
The core idea
ObservableObject is SwiftUI's original Combine-based observation model. A reference type publishes change events, usually through @Published, and SwiftUI re-renders views that depend on that object. Starting in iOS 17, Apple's Observation system introduces @Observable, which tracks property access more precisely and removes a lot of the manual publishing boilerplate.
The short interview answer is: ObservableObject broadcasts that an object changed, while @Observable lets SwiftUI track which properties a view actually read and invalidate more precisely when those properties change.
1. The old model: ObservableObject plus @Published
With the Combine-era approach, the model conforms to ObservableObject, and each property that should trigger UI updates must be marked @Published.
import SwiftUI
import Combine
final class ProfileModel: ObservableObject {
@Published var displayName = "Taylor"
@Published var unreadCount = 3
@Published var isPro = false
}This works, but it has two common costs:
- the model only updates the UI for properties you remembered to mark
@Published - the signal is broadly "something in this object changed", which is less precise as the model grows
That second point matters in larger screens. If several subviews depend on the same big view model, a single @Published change can cause more recomputation than the screen logically needs.
2. The new model: @Observable
With Observation, the macro generates the tracking machinery for you. Plain stored properties participate automatically, so the model becomes simpler to write and easier to evolve.
import Observation
@Observable
final class ProfileModel {
var displayName = "Taylor"
var unreadCount = 3
var isPro = false
}The important behavior change is not just less boilerplate. SwiftUI records which properties a view reads during body. If a view only reads unreadCount, then changing displayName does not need to invalidate that view.
That is why Observation scales better for wide models. It is closer to dependency tracking than object-wide broadcasting.
3. What @Observable does under the hood
Under the hood, @Observable is a macro, not magic. Per SE-0395: Observability, it synthesizes an ObservationRegistrar, rewrites stored properties into tracked computed properties, and uses helper calls to record reads and wrap mutations.
Conceptually, this:
import Observation
@Observable
final class ProfileModel {
var unreadCount = 3
}roughly becomes something like this:
import Observation
final class ProfileModel: Observable {
@ObservationIgnored
private let _$observationRegistrar = ObservationRegistrar()
@ObservationIgnored
private var _unreadCount = 3
var unreadCount: Int {
get {
access(keyPath: \.unreadCount)
return _unreadCount
}
set {
withMutation(keyPath: \.unreadCount) {
_unreadCount = newValue
}
}
}
}The important pieces are:
- reads call
access(keyPath:), which registers that this property was used - writes go through
withMutation(keyPath:), which tells the registrar that this property is changing - SwiftUI can then connect "this view read
unreadCount" to "this write changedunreadCount"
At a slightly deeper level, SwiftUI evaluates body inside withObservationTracking { ... }. That scope records which tracked properties were read while building the view. Later, when one of those properties mutates, the registrar triggers change handling for the views that actually depended on that key path. That is the mechanism behind the more targeted invalidation story.
4. The view-layer ownership story changes
ObservableObject usually appears with @StateObject, @ObservedObject, and @EnvironmentObject. On iOS 17+, @Observable models often use different wrappers:
- own the model with
@State - pass it down as a plain stored property or
let - use
@Bindablein a child that needs bindings such as$model.displayName - inject shared models with
@Environment
import SwiftUI
import Observation
@Observable
final class ProfileModel {
var displayName = "Taylor"
var isPro = false
}
struct ProfileScreen: View {
@State private var model = ProfileModel()
var body: some View {
ProfileEditor(model: model)
}
}
struct ProfileEditor: View {
@Bindable var model: ProfileModel
var body: some View {
TextField("Name", text: $model.displayName)
Toggle("Pro", isOn: $model.isPro)
}
}The ownership question still matters. SwiftUI still needs a stable place to keep the model alive. What changes is the observation mechanism, not the need to reason clearly about who owns the data and who only reads or edits it.
5. Why it matters
This difference shows up in real apps, not just in API style.
- large view models create less redraw churn when unrelated fields no longer invalidate the same subtrees
- teams write less boilerplate because they do not need
@Publishedon every reactive property - fewer refresh bugs slip through when a developer adds a new stored property and forgets to publish it
- feature models become easier to split or keep together based on domain boundaries rather than redraw workarounds
That last point is especially useful in interviews: many teams used to split one large ObservableObject into several smaller ones partly to reduce broad invalidation. Observation directly targets that pain.
Interview angle
Walk the answer in this order: ObservableObject is the old Combine model -> @Published manually opts properties into updates -> @Observable uses tracked property reads plus mutation tracking -> that gives SwiftUI a more precise dependency model -> the view-side wrappers also change (@State, @Bindable, @Environment) even though ownership still matters.