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 @Published properties; the macro observes all stored properties automatically.
  • Explicit ownership: Store @Observable references as @State in 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 @StateObject with @State private var.
  • If the model is injected: Pass it as a regular property via init or use @Environment/@Binding as 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 @Observable instead of conforming to ObservableObject.
  • @Published is 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 ObservableObject protocol conformance and @Published decorators.
  • Annotate the class with @Observable and import the Observation framework.
  • Store references using @State instead of @StateObject in the creating view.
  • Use conditional compilation with #if canImport(Observation) to support iOS 16 and earlier.
  • Reference swiftui-view-refactor/SKILL.md and swiftui-ui-patterns/SKILL.md for 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →