Contents

Migrating from the Observable Object protocol to the Observable macro

Update your existing app to leverage the benefits of Observation in Swift.

Overview

Starting with iOS 17, iPadOS 17, macOS 14, tvOS 17, and watchOS 10, SwiftUI provides support for Observation, a Swift-specific implementation of the observer design pattern. Adopting Observation provides your app with the following benefits:

  • Tracking optionals and collections of objects, which isn’t possible when using ObservableObject.

  • Using existing data flow primitives like State and Environment instead of object-based equivalents such as StateObject and EnvironmentObject.

  • Updating views based on changes to the observable properties that a view’s body reads instead of any property changes that occur to an observable object, which can help improve your app’s performance.

To take advantage of these benefits in your app, you’ll discover how to replace existing source code that relies on ObservableObject with code that leverages the Observable() macro.

Use the Observable macro

To adopt Observation in an existing app, begin by replacing ObservableObject in your data model type with the Observable() macro. The Observable() macro generates source code at compile time that adds observation support to the type.

Then remove the Published property wrapper from observable properties. Observation doesn’t require a property wrapper to make a property observable. Instead, the accessibility of the property in relationship to an observer, such as a view, determines whether a property is observable.

If you have properties that are accessible to an observer that you don’t want to track, apply the ObservationIgnored() macro to the property.

Migrate incrementally

You don’t need to make a wholesale replacement of the ObservableObject protocol throughout your app. Instead, you can make changes incrementally. Start by changing one data model type to use the Observable() macro. Your app can mix data model types that use different observation systems. However, SwiftUI tracks changes differently based on the observation system that a data model type uses, Observable versus ObservableObject.

You may notice slight behavioral differences in your app based on the tracking method. For instance, when tracking as Observable(), SwiftUI updates a view only when an observable property changes and the view’s body reads the property directly. The view doesn’t update when observable properties not read by body changes. In contrast, a view updates when any published property of an ObservableObject instance changes, even if the view doesn’t read the property that changes, when tracking as ObservableObject.

Migrate other source code

The only change made to the sample app so far is to apply the Observable() macro to Library and remove support for the ObservableObject protocol. The app still uses the ObservableObject data flow primitive like StateObject to manage an instance of Library. If you were to build and run the app, SwiftUI still updates the views as expected. That’s because data flow property wrappers such as StateObject and EnvironmentObject support types that use the Observable() macro. SwiftUI provides this support so apps can make source code changes incrementally.

However, to fully adopt Observation, replace the use of StateObject with State after updating your data model type. For example, in the following code the main app structure creates an instance of Library and stores it as a StateObject. It also adds the Library instance to the environment using the environmentObject(_:) modifier.

// BEFORE
@main
struct BookReaderApp: App {
    @StateObject private var library = Library()

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environmentObject(library)
        }
    }
}

Now that Library no longer conforms to ObservableObject, the code can change to use State instead of StateObject and to add library to the environment using the environment(_:) modifier.

// AFTER
@main
struct BookReaderApp: App {
    @State private var library = Library()

    var body: some Scene {
        WindowGroup {
            LibraryView()
                .environment(library)
        }
    }
}

One more change must happen before Library fully adopts Observation. Previously the view LibraryView retrieved a Library instance from the environment using the EnvironmentObject property wrapper. The new code, however, uses the Environment property wrapper instead.

Remove the ObservedObject property wrapper

To wrap up the migration of the sample app, change the data model type Book to support Observation by removing ObservableObject from the type declaration and apply the Observable() macro. Then remove the Published property wrapper from observable properties.

Next, remove the ObservedObject property wrapper from the book variable in the BookView. This property wrapper isn’t needed when adopting Observation. That’s because SwiftUI automatically tracks any observable properties that a view’s body reads directly. For example, SwiftUI updates BookView when book.title changes.

However, if a view needs a binding to an observable type, replace ObservedObject with the Bindable property wrapper. This property wrapper provides binding support to an observable type so that views that expect a binding can change an observable property. For instance, in the following code TextField receives a binding to book.title:

See Also

Creating model data