Migrating to Observable

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 and Environment. You no longer need StateObject or EnvironmentObject.
  • 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

  1. Remove the ObservableObject conformance and add the Observable macro:

    // import Combine
    import Observation
    
    // final class AppSettings: ObservableObject {
    @Observable final class AppSettings {
    
  2. Remove the Published property wrapper from any observable properties:

      // @Published var confirmDeletion = true
      var confirmDeletion = true
    
  3. 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…

Read More