The Question Mark - blog by Mark Volkmann

Observation

Overview

Observation enables defining model objects using Swift syntax and triggering SwiftUI updates based on changes to model objects.

Resources

@Binding

The @Binding property wrapper allows a view to mutate data that is owned by another view, typically its parent view.

The following code implements a checkbox view:

import SwiftUI

struct Checkbox: View {
    var label: String
    @Binding var isOn: Bool

    var body: some View {
        // This approach only works in macOS.
        // Toggle(isOn: $isOn) { Text(label) }.toggleStyle(.checkbox)

        Button {
            isOn.toggle()
        } label:
            Image(systemName: isOn ? "checkmark.square" : "square")
            Text(label)
        }
        .buttonStyle(.plain)
    }
}

The following code demonstrates using the Checkbox view:

struct ContentView: View {
    @State private var isHappy = false

    var body: some View {
        VStack(alignment: .leading) {
            Checkbox(label: "Happy?", isOn: $isHappy)
            Text(isHappy ? "Good for you!" : "Maybe tomorrow.")
        }
        .padding()
    }
}

@Observable

To define a data model, apply the Observable macro to a class definition. For example:

@Observable class TodosModel {
    var todos: [Todo] = []
}

@Bindable

The @Bindable property wrapper creates a two-way binding to instance of a class to which the @Observable macro is applied. This binding can be used to get and set properties in the object.

The app ObservableDemo demonstrates this. It defines a ViewModel as follows:

import Observation

@Observable
class ViewModel {
    var todos: [Todo] = [
        Todo("Cut grass"),
        Todo("Buy Milk", done: true)
    ]

    func addTodo(_ todo: Todo) {
        todos.append(todo)
    }

    func deleteTodo(_ todo: Todo) {
        todos.removeAll { $0 == todo }
    }
}

The Todo type is defined as follows:

import Foundation // for UUID
import Observation

// This needs to be a class instead of a struct
// in order to apply the @Observable macro.
@Observable
class Todo: Equatable, Identifiable {
    var description = ""
    var done = false
    let id: UUID = .init()

    init(_ description: String, done: Bool = false) {
        self.description = description
        self.done = done
    }

    static func == (lhs: Todo, rhs: Todo) -> Bool {
        lhs.id == rhs.id:w
    }
}

The main view is defined as follows:

import Observation
import SwiftUI

struct ContentView: View {
    @Environment(ViewModel.self) private var vm
    @State private var description = ""

    var body: some View {
        VStack(alignment: .leading) {
            HStack {
                TextField("todo description", text: $description)
                    .textInputAutocapitalization(.never)
                    .textFieldStyle(.roundedBorder)
                Button("Add") {
                    vm.addTodo(Todo(description))
                    description = ""
                }
                .buttonStyle(.borderedProminent)
                .disabled(description.isEmpty)
            }

            List {
                // The `sortedInsensitive` method is defined in
                // Extensions/SequenceExtension.swift.
                ForEach(vm.todos.sortedInsensitive(by: \.description)) { todo in
                    TodoRow(todo: todo)
                }
            }
            .listStyle(.plain)
        }
        .padding()
    }
}

The TodoRow view takes a binding to an instance of the Todo class to which the @Observable macro is applied. A binding to the done property of the Todo object is passed to the Checkbox view which can modify that property.

import Observation
import SwiftUI

struct TodoRow: View {
    @Bindable var todo: Todo
    @Environment(ViewModel.self) private var vm

    var body: some View {
        HStack {
            Checkbox(label: todo.description, isOn: $todo.done)
                .strikethrough(todo.done)
            Spacer()
            Button {
                vm.deleteTodo(todo)
            } label: {
                Image(systemName: "trash")
            }
            // Without this, tapping any button triggers
            // all buttons in the same HStack!
            .buttonStyle(.borderless)
        }
    }
}

The Checkbox view is defined as follow:

import SwiftUI

struct Checkbox: View {
    var label: String
    @Binding var isOn: Bool

    var body: some View {
        Button {
            isOn.toggle( gg
        } label: {
            Image(systemName: isOn ? "checkmark.square" : "square")
            Text(label)
        }
        .buttonStyle(.plain)
    }
}

Example App

See ObservableDemo.