# How to Convert ObservableObject to @Observable for iOS 17+: A Complete Migration Guide

> Migrate your iOS 17+ apps from ObservableObject to @Observable. Learn to replace class declarations, @Published properties, and @StateObject with @State for a seamless transition.

- Repository: [Thomas Ricouard/Skills](https://github.com/Dimillian/Skills)
- Tags: migration-guide
- Published: 2026-04-01

---

**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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/swiftui-ui-patterns/references/lightweight-clients.md).

### Classic ObservableObject (iOS 16 and Earlier)

```swift
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+)

```swift
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

```swift
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`](https://github.com/Dimillian/Skills/blob/main/swiftui-view-refactor/SKILL.md) and [`swiftui-ui-patterns/SKILL.md`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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.