# @Observable (iOS 17+) in SwiftUI

**Full code available on** [**here**](https://github.com/xavier7t/iOSDevX/blob/main/iOSDevX/202611-Nov%202026/DemoObservable20261114.swift)**.**

Since iOS 17, the `@Observable` macro lets you build observable models without `ObservableObject`, `@Published`, `@StateObject` or `@ObservedObject`. In this post we build a small task list that uses `@Observable`, `@Bindable` and `@State` to show how the new model works.

## Why use @Observable?

1. **Less boilerplate.** No `@Published` on every property. Every stored property is tracked automatically.
2. **More precise updates.** A view re-renders only when a property it actually reads changes. With `ObservableObject`, any `@Published` change refreshed every view observing the object.
3. **Simpler property wrappers.** `@State` owns the model, and `@Bindable` creates bindings. You no longer need `@StateObject`, `@ObservedObject` or `@EnvironmentObject` for the common cases.

## Step-by-step implementation

### Step 1: Create an @Observable model

```swift
@available(iOS 17, *)
@Observable
class Task: Identifiable {
    let id = UUID()
    var title: String
    var isDone = false

    init(title: String) {
        self.title = title
    }
}
```

Add `@Observable` to a class (it must be a class) and `import Observation`. There is no `ObservableObject` conformance and no `@Published`. Each stored property is observable.

### Step 2: Create a store

```swift
@available(iOS 17, *)
@Observable
class TaskStore {
    var tasks: [Task] = [
        Task(title: "Read about Observation"),
        Task(title: "Replace ObservableObject")
    ]

    var remaining: Int {
        tasks.filter { !$0.isDone }.count
    }

    func add(_ title: String) {
        tasks.append(Task(title: title))
    }
}
```

Computed properties like `remaining` are tracked too. Any view that reads it updates when a task's `isDone` changes.

### Step 3: Bind to properties with @Bindable

```swift
@available(iOS 17, *)
struct TaskRow: View {
    @Bindable var task: Task

    var body: some View {
        HStack {
            Toggle("", isOn: $task.isDone)
                .labelsHidden()
            TextField("Title", text: $task.title)
                .strikethrough(task.isDone)
        }
    }
}
```

A plain `let` or `var` can read an `@Observable` object, but it can't give you `$` bindings. `@Bindable` adds them, so `$task.isDone` and `$task.title` work with `Toggle` and `TextField`.

### Step 4: Own the store with @State

```swift
@available(iOS 17, *)
struct TaskListView: View {
    @State private var store = TaskStore()
    @State private var newTitle = ""

    var body: some View {
        NavigationStack {
            List {
                Section("\(store.remaining) remaining") {
                    ForEach(store.tasks) { task in
                        TaskRow(task: task)
                    }
                }
                Section {
                    TextField("New task", text: $newTitle)
                        .onSubmit {
                            guard !newTitle.isEmpty else { return }
                            store.add(newTitle)
                            newTitle = ""
                        }
                }
            }
            .navigationTitle("Tasks")
        }
    }
}
```

Use `@State` to create and own an `@Observable` object, in the place where you would have used `@StateObject`. The view keeps the same instance across re-renders.

## Example usage

Because the project targets iOS 16.4, we wrap the view in an availability check with a fallback:

```swift
struct ContentView: View {
    var body: some View {
        if #available(iOS 17, *) {
            TaskListView()
        } else {
            Text("@Observable requires iOS 17 or later.")
                .padding()
        }
    }
}
```

If your app's minimum is iOS 17, you can drop the `@available` attributes and the fallback.

## Key components explained

1. `@Observable`
   - A macro that makes a class's stored properties observable.
   - Replaces `ObservableObject` plus `@Published`.
2. `@Bindable`
   - Creates `$` bindings to an `@Observable` object's properties.
   - Needed for controls like `Toggle` and `TextField`.
3. `@State`
   - Owns an `@Observable` instance for the view's lifetime.
   - Replaces `@StateObject`.
4. `@available(iOS 17, *)`
   - Observation needs iOS 17, so older deployment targets need a guard and a fallback.

## Benefits

- **Less code.** No `@Published`, no `objectWillChange`.
- **Fewer re-renders.** Views update only for the properties they read.
- **Easier mental model.** Fewer property wrappers to choose between.

## Conclusion

`@Observable` is the modern way to share state in SwiftUI. Mark a class with the macro, own it with `@State`, and use `@Bindable` when you need bindings. If you are still using `ObservableObject`, it is worth migrating one model at a time.

*If you found this article helpful, please consider supporting my work on* [*Buy Me a Coffee*](https://buymeacoffee.com/xavierios)*. Don't forget to subscribe to my newsletter for more tutorials like this!*
