Transitioning to @Observable in SwiftUI
A guide on how to adopt the new Observation framework for SwiftUI apps
The Observation Framework
One of the biggest changes to Swift and SwiftUI in iOS 17 was the introduction of the Observable macro. This macro lets an object update any observers whenever its properties change. To add observation support to a type, all you have to is attach the @Observable macro:
@Observable
class Post {
var title: String
var description: String
var publishedAt: Date
init(title: String, description: String, publishedAt: Date) {
// ...
}
}
With SwiftUI, where UI = f(State)
, the Observation framework fits in
perfectly, as Views can redraw efficiently based on state changes in an
Observable-conforming model. Let’s take a look at how SwiftUI is able to
incorporate Observable in iOS 17.
@Observable vs ObservableObject
The easiest way to get started transitioning to the Observation framework is to convert your ObservableObject conformances into @Observable. Let’s start with an ObservableObject view model to hold a list of posts:
class PostsViewModel: ObservableObject {
@Published var posts = [Post]()
}
To convert this view model into an @Observable object, all you need to do is the following:
@Observable
class PostsViewModel {
var posts = [Post]()
}
And ta-da! Now this view model’s posts
property will automatically share any
changes with its observers. In this case, that means our SwiftUI views.
Ignoring properties
If there’s a property that you don’t want to track, you can use the ObservationIgnored macro inside of the Observable object.
@Observable
class PostsViewModel {
var posts = [Post]()
@ObservationIgnored
private var dataSource = PostsDataSource()
}
Any changes made to the dataSource
property will now no longer update
observers.
@State vs @StateObject
To use an @Observable object in a SwiftUI View, we simply need to change it
from a @StateObject to @State. Using our updated PostsViewModel
, we
can see this here:
struct PostsView: View {
// We use @State instead of @StateObject now
@State private var viewModel = PostsViewModel()
var body: some View {
ScrollView {
LazyVStack {
// This will update anytime posts changes!
ForEach(viewModel.posts) { post in
// ...
}
}
}
}
}
@Bindable vs @ObservedObject
If we want to pass down our view model to a child view from this parent view, we
can remove the @ObservedObject property wrapper because SwiftUI
automatically tracks any @Observable properties that body
reads directly.
struct PostsChildView: View {
// We can remove @ObservedObject
var viewModel: PostsViewModel
var body: some View {
Text(String(viewModel.posts.count))
}
}
However, if a view needs a binding from the @Observable type, we can replace @ObservedObject with @Bindable. In this case, we may have something like this:
struct PostsChildView: View {
// Replace @ObservedObject with @Bindable
@Bindable var viewModel: PostsViewModel
var body: some View {
SomeBindableView(posts: $viewModel.posts)
}
}
@Environment vs @EnvironmentObject
Lastly, we can look at one of the best DX changes in SwiftUI with Observation.
Instead of passing down a @StateObject with the .environmentObject(_:)
modifier, we can now pass down a @State observable with the
.environment(_:)
modifier.
In order to read from the environment, however, we must specify the observable’s type.
struct PostsView: View {
@State private var viewModel = PostsViewModel()
var body: some View {
ScrollView {
// ...
}
.environment(viewModel)
}
}
struct PostsChildView: View {
// Remember to specify the type!
@Environment(PostsViewModel.self) private var viewModel
var body: some View {
// ...
}
}
Incremental Changes
If you don’t feel like adopting all of these changes at once, SwiftUI allows you to adopt them incrementally. @StateObject and @EnvironmentObject support types that use the Observable macro, which means you can start by removing ObservedObject and then update your views later.
A Cheat Sheet
Old | New | When? | Where? |
---|---|---|---|
@StateObject | @State | Anytime | View |
@ObservedObject | No wrapper | When body only reads the value | View |
@ObservedObject | @Bindable | When a body view needs a binding | View |
@EnvironmentObject | @Environment | Anytime | View |
ObservableObject | @Observable | Anytime | Model |
That’s the Tea
Thanks for reading! Hopefully you learned how to use the new Observation framework in your future SwiftUI projects. If you’d like to learn more, you can check out Apple’s documentation.