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 😅