SwiftUI @Observable vs ObservableObject

Updated Jun 06, 2025#swiftui#comparison

Recently very often I have to decide whether to use new @Observable macro in Observation framework or stick to old ObservableObject protocol in Combine framework.

SwiftUI uses Observer Pattern heavily under the hood. This behavioral design pattern lets one object (the subject) notify other objects (the observers) about changes to its state, without knowing who those observers are. It’s often used to implement event systems, data bindings, or reactive programming. In SwiftUI, views act as observers of your model’s state, and automatically update when the data changes.

ObservableObject is a protocol that allows you to create a reference type (usually a class) whose changes can be observed by views. It’s part of the Combine framework and is one of the core building blocks for managing shared, reactive state in SwiftUI since iOS 13.

import SwiftUI
import Combine

class CounterModel: ObservableObject {
    @Published var count = 0
}

struct CounterView: View {
    @StateObject var model = CounterModel()

    var body: some View {
        VStack {
            Text("Count: \(model.count)")
            Button("Increment") {
                model.count += 1
            }
            Button("Decrement") {
                model.count -= 1
            }
        }
    }
}

The @Observable macro, in addition to adding observation functionality, also conforms your data model type to the Observable protocol, which serves as a signal to other APIs that your type supports observation. Don’t apply the Observable protocol by itself to your data model type, since that alone doesn’t add any observation functionality. Instead, always use the Observable macro when adding observation support to your type.

import SwiftUI
import Observation

@Observable
class CounterModel {
    var count = 0
}

struct CounterView: View {
    @State var model = CounterModel()

    var body: some View {
        VStack {
            Text("Count: \(model.count)")
            Button("Increment") {
                model.count += 1
            }
            Button("Decrement") {
                model.count -= 1
            }
        }
    }
}

Here’s a detailed comparison between the two:

Feature @Observable ObservableObject
Introduced In iOS 17, macOS 14 iOS 13, macOS 10.15
Framework Observation Combine
Type Requirements Class Class
Observable Property Marker All vars are observed, opt out with @ObservationIgnored Opt in with @Published
View Integration @State
@Bindable
@Environment
@StateObject
@ObservedObject
@EnvironmentObject
Tracking optionals Yes No
Tracking collections of objects Yes No
Updating views based on Changes to the observable properties that a view’s body reads. Any property changes that occur to an observable object.

Data flow property wrappers such as @StateObject and @EnvironmentObject support types that use the @Observable macro. SwiftUI provides this support so apps can make source code changes incrementally. However, to fully adopt Observation, replace the use of @StateObject with @State after updating your data model type.

Initially @Observable macro was advertised to work with both class and struct but that’s not the case anymore. For now @Observable only works with classes (not structs) because of how SwiftUI tracks state changes — it needs reference semantics to observe and propagate changes efficiently. There’s ongoing discussion in the Swift forums, but struct support is unlikely without a major change to Swift’s value-type semantics.

I’ll explore the difficulties of accessing UserDefaults, or using property wrappers within @Observable in another post.