10 Step Guide: SwiftUI ObservableObject to Observable Macro Transition

10 Step Guide: SwiftUI ObservableObject to Observable Macro Transition

In my last article, I talked about how data flows in SwiftUI. Now, Apple has released something new in iOS 17 - the Observation Framework. This changes how SwiftUI works with data. In this article, we will look at how to use the new Observable Macro in your SwiftUI code.

Step 1: Understand the Observable Macro

Observable Macro generates code automatically for you to observe data changes. Compared to the old ObservableObject protocol, this feature is more powerful as it tracks changes in optional and collection data types and enhances your app's performance by avoiding unnecessary updates.

Step 2: Import the Observation Framework

Start by importing the new Observation framework. You'll then need to replace the ObservableObject with the Observable macro. For example, the class "MovieLibrary" was an ObservableObject will become an Observable:

// BEFORE
import SwiftUI

class MovieLibrary: ObservableObject {
    // ...
}

// AFTER
import SwiftUI
import Observation

@Observable class MovieLibrary {
    // ...
}

Step 3: Remove the Published Wrapper

Before, you had to wrap observable properties with Published. But the Observable macro takes care of that. So, you can remove the Published wrapper. For instance:

// BEFORE
@Observable class MovieLibrary {
    @Published var movies: [Movie] = [Movie(), Movie(), Movie()]
}

// AFTER
@Observable class MovieLibrary {
    var movies: [Movie] = [Movie(), Movie(), Movie()]
}

Step 4: Ignore Properties If Needed

What if you don't want to track certain properties? Just use the ObservationIgnored macro:

@Observable class MovieLibrary {
    @ObservationIgnored var somePropertyNotToTrack: Int = 0
    var movies: [Movie] = [Movie(), Movie(), Movie()]
}

Step 5: Start Migration Gradually

SwiftUI allows you to change your code bit by bit. You can start by converting one data model to use the Observable macro and then move to others. But, remember that the way SwiftUI tracks changes might be slightly different now.

Step 6: Replace StateObject with State

Once you've converted all data models to use the Observable macro, replace StateObject with State:

// BEFORE
@main
struct MovieApp: App {
    @StateObject private var movieLibrary = MovieLibrary()

    var body: some Scene {
        WindowGroup {
            MovieLibraryView()
                .environmentObject(movieLibrary)
        }
    }
}

// AFTER
@main
struct MovieApp: App {
    @State private var movieLibrary = MovieLibrary()

    var body: some Scene {
        WindowGroup {
            MovieLibraryView()
                .environment(movieLibrary)
        }
    }
}

Step 7: Replace EnvironmentObject with Environment

Next, you should replace the EnvironmentObject property wrapper with Environment:

// BEFORE
struct MovieLibraryView: View {
    @EnvironmentObject var movieLibrary: MovieLibrary

    var body: some View {
        List(movieLibrary.movies) { movie in
            MovieView(movie: movie)
        }
    }
}

// AFTER
struct MovieLibraryView: View {
    @Environment(MovieLibrary.self) private var movieLibrary

    var body: some View {
        List(movieLibrary.movies) { movie in
            MovieView(movie: movie)
        }
    }
}

Step 8: Remove the ObservedObject Wrapper

Now it's time to remove the ObservedObject property wrapper. 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:

// BEFORE
struct MovieView: View {
    @ObservedObject var movie: Movie
    @State private var isEditorPresented = false

    var body: some View {
        HStack {
            Text(movie.title)
            Spacer()
            Button("Edit") {
                isEditorPresented = true
            }
        }
        .sheet(isPresented: $isEditorPresented) {
            MovieEditView(movie: movie)
        }
    }
}

// AFTER
struct MovieView: View {
    var movie: Movie
    @State private var isEditorPresented = false

    var body: some View {
        HStack {
            Text(movie.title)
            Spacer()
            Button("Edit") {
                isEditorPresented = true
            }
        }
        .sheet(isPresented: $isEditorPresented) {
            MovieEditView(movie: movie)
        }
    }
}

Step 9: Use the Bindable Wrapper

If your view needs a binding to an observable type, replace ObservedObject with the Bindable property wrapper:

// BEFORE
struct MovieEditView: View {
    @ObservedObject var movie: Movie
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack() {
            TextField("Title", text: $movie.title)
                .textFieldStyle(.roundedBorder)
                .onSubmit {
                    dismiss()
                }

            Button("Close") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

// AFTER
struct MovieEditView: View {
    @Bindable var movie: Movie
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        VStack() {
            TextField("Title", text: $movie.title)
                .textFieldStyle(.roundedBorder)
                .onSubmit {
                    dismiss()
                }

            Button("Close") {
                dismiss()
            }
            .buttonStyle(.borderedProminent)
        }
        .padding()
    }
}

Step 10: Congratulations - You've Migrated!

By following these steps, you've just migrated to the Observable macro in SwiftUI. Great job! You've enhanced your app's performance and can now track changes in your data more effectively.

You may also like: 7 Key Strategies for Reducing Code Duplication in SwiftUI

I hope you enjoyed this article, and if you have any questions, comments, or feedback, then feel free to comment here or reach out via Twitter.

Thanks for reading!