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
State
andEnvironment
. You no longer needStateObject
orEnvironmentObject
. - Less boiler-plate. No need to annotate properties with
@Published
or 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
ObservableObject
conformance and add theObservable
macro:// import Combine import Observation // final class AppSettings: ObservableObject { @Observable final class AppSettings {
-
Remove the
Published
property 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
ObservationIgnored
macro:@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…