How to Convert ObservableObject to @Observable for iOS 17+: A Complete Migration Guide
To convert ObservableObject to @Observable for iOS 17+, replace the class declaration with @Observable, remove @Published properties, and change @StateObject to @State in the owning view.
The Dimillian/Skills repository documents modern SwiftUI architecture patterns, including the transition from the Combine-based ObservableObject protocol to the new Observation framework. When you convert ObservableObject to @Observable for iOS 17 and later, you eliminate boilerplate @Published declarations while gaining granular change tracking that reduces unnecessary view updates.
Why Migrate to @Observable?
The Observation framework introduced in iOS 17 replaces the legacy ObservableObject/@StateObject pattern with the @Observable macro. According to the repository's swiftui-view-refactor/SKILL.md, this migration offers three architectural advantages:
- Granular change tracking: The Observation runtime automatically detects which individual properties changed, reducing view recomputation compared to the coarse-grained updates of
ObservableObject. - Less boilerplate: You no longer need to expose
@Publishedproperties; the macro observes all stored properties automatically. - Explicit ownership: Store
@Observablereferences as@Statein the owning view, making the data flow explicit and preventing accidental retain cycles.
Step-by-Step Migration Guide
Follow these steps documented in swiftui-view-refactor/SKILL.md (lines 78-82) to migrate your data models.
Step 1 - Remove ObservableObject Conformance
Identify your existing ObservableObject class and remove the protocol conformance. You do not need to replace it with another protocol.
Step 2 - Add the @Observable Macro
Annotate the class declaration with @Observable. This macro expands to provide the observation capabilities previously handled by ObservableObject and @Published.
Remove all @Published property wrappers. Under the Observation framework, all stored properties are automatically observed.
Step 3 - Update Property Wrappers in Views
Change how your view holds the model reference:
- If the view creates the model: Replace
@StateObjectwith@State private var. - If the model is injected: Pass it as a regular property via
initor use@Environment/@Bindingas appropriate.
As noted in swiftui-ui-patterns/SKILL.md (lines 49-51), @State now serves as the root-owned reference for @Observable models on iOS 17+.
Step 4 - Handle Legacy iOS Versions (Optional)
When supporting iOS 16 or earlier, maintain backward compatibility using conditional compilation. Wrap the @Observable implementation in #if canImport(Observation) checks, keeping the ObservableObject version as a fallback.
Code Examples
These examples align with the patterns found in swiftui-ui-patterns/references/lightweight-clients.md.
Classic ObservableObject (iOS 16 and Earlier)
import SwiftUI
final class ItemsStore: ObservableObject {
@Published var items: [Item] = []
func load() async {
// fetch items …
}
}
struct ContentView: View {
@StateObject private var store = ItemsStore()
var body: some View {
List(store.items) { item in
Text(item.title)
}
.task { await store.load() }
}
}
Modern @Observable (iOS 17+)
import Observation
import SwiftUI
@Observable final class ItemsStore {
var items: [Item] = []
func load() async {
// fetch items …
}
}
struct ContentView: View {
@State private var store = ItemsStore()
var body: some View {
List(store.items) { item in
Text(item.title)
}
.task { await store.load() }
}
}
Key changes in this refactored version:
- The class uses
@Observableinstead of conforming toObservableObject. @Publishedis removed—all stored properties trigger updates automatically.- The view owns the store via
@State, satisfying the root-owned reference requirement for iOS 17+.
Conditional Compilation for Mixed Deployment
import SwiftUI
#if canImport(Observation)
import Observation
@Observable final class ItemsStore {
var items: [Item] = []
func load() async { /* … */ }
}
#else
final class ItemsStore: ObservableObject {
@Published var items: [Item] = []
func load() async { /* … */ }
}
#endif
struct ContentView: View {
#if canImport(Observation)
@State private var store = ItemsStore()
#else
@StateObject private var store = ItemsStore()
#endif
var body: some View {
List(store.items) { item in
Text(item.title)
}
.task { await store.load() }
}
}
This pattern allows a single codebase to compile for both iOS 17+ (using @Observable) and earlier versions (using ObservableObject).
Summary
Follow these key takeaways when you convert ObservableObject to @Observable:
- Remove
ObservableObjectprotocol conformance and@Publisheddecorators. - Annotate the class with
@Observableand import theObservationframework. - Store references using
@Stateinstead of@StateObjectin the creating view. - Use conditional compilation with
#if canImport(Observation)to support iOS 16 and earlier. - Reference
swiftui-view-refactor/SKILL.mdandswiftui-ui-patterns/SKILL.mdfor architectural guidance.
Frequently Asked Questions
Do I need to import Observation to use @Observable?
Yes. You must explicitly import Observation at the top of your Swift file when using the @Observable macro. This import provides the macro definition and the underlying observation runtime that tracks property changes.
Can I use @Observable and ObservableObject in the same project?
Yes. You can gradually migrate individual classes while keeping legacy ObservableObject implementations. The repository recommends using #if canImport(Observation) checks to maintain a single model implementation that compiles under different iOS versions, or maintaining separate models for different deployment targets.
Why does @State replace @StateObject for @Observable models?
In iOS 17+, @State gained the ability to track observation changes through the new Observation framework. Since @Observable classes no longer conform to ObservableObject, they cannot use @StateObject or @ObservedObject. Instead, @State creates and owns the reference while automatically subscribing to observation notifications, as documented in swiftui-view-refactor/SKILL.md.
How do I pass an @Observable model to child views?
Pass @Observable models as regular properties through initializers or use @Environment for dependency injection. Unlike ObservableObject, you do not need @ObservedObject in child views; SwiftUI automatically tracks which properties are accessed in the view body and updates accordingly when those specific properties change.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →