read

The introduction of Observation framework changed the way we use SwiftUI, yet again.

Before iOS 17, the use of State, StateObject and others is a little more complex than necessary, while there are unsupported feature such as nested observation.

I will not explain the details, as you can read the Apple documentation and the sample code on how the new observation framework works.

Instead, I will provide some code snippets for an app settings view, which is a very common feature.

The Model

Let’s say the app has a setting for the user’s preferred photo size to store. We use an enum which will be usable with a Picker later on.

enum PhotoSize: String, CaseIterable, Identifiable {
    case small = "S"
    case medium = "M"
    case large = "L"

    var id: Self { self }
}

We can model all the settings in a class with the @Observable macro. In doing so, every setting in the class will be observable.

@Observable class AppSettings {
    var photoSize: PhotoSize = .medium
    // Other settings..
}

The App

An app settings is a global object, and it can be store with @State in App, and passed via environment.

@main struct MyApp: App {
    @State var settings = AppSettings() // The source of truth in App

    var body: some Scene {
        ContentView()
        .environment(settings)

        Settings {
            SettingsView()
        }
        .environment(settings)
    }
}

The View

The view has access to settings via environment as usual.

But to access Binding<PhotoSize> for the picker to use, you need the help of the new @Bindable.

struct SettingsView: View {
    @Environment(AppSettings.self) var settings: AppSettings

    var body: some View {
        @Bindable var settings = settings // Yup, you can set in body
        Form {
            Picker("Preferred Photo Size:", selection: $settings.photoSize) { // Access the binding
                ForEach(PhotoSize.allCases) { option in
                    Text(option.rawValue)
                }
            }
        }
    }
}

Bonus: AppStorage in Observable

So far, our model does not persist the user’s selection. Usually, you may use AppStorage to persist.

@Observable class AppSettings {
    @AppStorage("photoSize") var photoSize: PhotoSize = .medium
}

HOWEVER, that won’t work, for now.

AppStorage often doesn’t play well in SwiftUI, and in this case it can’t compile in the shining new Observable class. You may add @ObservationIgnored, but then it won’t observe.

To only way I know is to fallback to regular UserDefaults, and providing the observation calls manually like this:

@Observable
class AppSettings {

    @ObservationIgnored // 1. We will handle this property manually
    var photoSize: PhotoSize {
        get {
            access(keyPath: \.photoSize) // 2. Access property
            if let s = UserDefaults.standard.string(forKey: "photoSize") {
                return PhotoSize(rawValue: s) ?? .medium
            }
            return .medium
        }
        set {
            withMutation(keyPath: \.photoSize) { // 3. willSet and didSet property
                UserDefaults.standard.setValue(newValue.rawValue, forKey: "photoSize")
            }
        }
    }

}

Or continue to use the old ObservableObject, or use another macro, for the time being while Apple is fixing it 😅


Image

@samwize

¯\_(ツ)_/¯

Back to Home