A quick guide to migrating an ObservableObject to Observable.
Observable
Apple introduced the Observable protocol in iOS 17 and macOS 14. This is a Swift macro based replacement to the Combine based ObservableObject. It has a number of benefits:
- Simplified data flow, using
StateandEnvironment. You no longer needStateObjectorEnvironmentObject. - Less boiler-plate. No need to annotate properties with
@Publishedor models with@ObservedObject. - A view is now only updated when a property it depends on changes not when any change happens to the observable object.
- You can have arrays of observable models or even observable types that contain other observable types.
Apple has a useful guide on the steps to migrate an ObservableObject to an Observable. To summarise:
Replace ObservableObject Conformance
-
Remove the
ObservableObjectconformance and add theObservablemacro:// import Combine import Observation // final class AppSettings: ObservableObject { @Observable final class AppSettings { -
Remove the
Publishedproperty wrapper from any observable properties:// @Published var confirmDeletion = true var confirmDeletion = true -
Any accessible property in the object is now treated as observable unless you exclude it with the
ObservationIgnoredmacro:@ObservationIgnored var doNotTrack = false
Using the Object
When creating the model object, you can replace StateObject with State:
// @StateObject var appSettings = AppSettings()
@State var appSettings = AppSettings()
When passing the object to a view you no longer need to mark the object with the ObservedObject property wrapper:
struct SettingsView: View {
// @ObservedObject var appSettings: AppSettings
var appSettings: AppSettings
var body: some View {
LabeledContent("Confirm deletion",
value: appSettings.confirmDeletion ? "Yes" : "No")
}
}
@Bindable
When removing the ObservedObject property wrapper add @Bindable if you need a binding to make changes to a property in the model object:
struct SettingsView: View {
@Bindable var appSettings: AppSettings
var body: some View {
Toggle("Confirm deletion",
isOn: $appSettings.confirmDeletion)
}
}
Using the Environment
If you’re passing the object on the environment, replace the environmentObject modifier with the new environment modifier which accepts an Observable:
SettingsView()
// .environmentObject(appSettings)
.environment(appSettings)
In the view, replace EnvironmentObject with Environment anytime you need to get the Observable model from the environment:
struct SettingsView: View {
// @EnvironmentObject var appSettings: AppSettings
@Environment(AppSettings.self) var appSettings
Binding to an Environment Object?
One final tip. What happens if I want a binding to a property of my model that I’ve passed on the environment? This had me stuck for a while, here’s what I want to write:
struct SettingsView: View {
@Environment(AppSettings.self) var appSettings
var body: some View {
// Cannot find $appSettings in scope
Toggle("Confirm deletion",
isOn: $appSettings.confirmDeletion)
}
}
That doesn’t work. I want to declare my app settings object using both the @Bindable and @Environment property wrappers but that’s not allowed. The solution is to create a bindable to the model object in the view body:
struct SettingsView: View {
@Environment(AppSettings.self) var appSettings
var body: some View {
// Create a bindable appSettings
@Bindable var appSettings = appSettings
Toggle("Confirm deletion",
isOn: $appSettings.confirmDeletion)
}
}
That was not obvious to me…