Table of contents
- Step 1: Understand the Observable Macro
- Step 2: Import the Observation Framework
- Step 3: Remove the Published Wrapper
- Step 4: Ignore Properties If Needed
- Step 5: Start Migration Gradually
- Step 6: Replace StateObject with State
- Step 7: Replace EnvironmentObject with Environment
- Step 8: Remove the ObservedObject Wrapper
- Step 9: Use the Bindable Wrapper
- Step 10: Congratulations - You've Migrated!
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!