SwiftUI’s Bindable property wrapper explained
Published on: June 30, 2023WIth the introduction of Xcode 15 beta and its corresponding beta OSses (I would say iOS 17 beta, but of course we also get macOS, iPadOS, and other betas...) Apple has introduced new state mangement tools for SwiftUI. One of these new tools is the @Bindable
property wrapper. In an earlier post I explained that @Binding
and @Bindable
do not solve the same problem, and that they will co-exist in your applications. In this post, I would like to clarify the purpose and the use cases for @Bindable
a little bit better so that you can make better decisions when picking your SwiftUI state property wrappers.
If you prefer learning by video, the key lessons from this blog post are also covered in this video:
The key purpose of the @Bindable
is to allow developers to create bindings to properties that are part of a model that confoms to the Observable
protocol. Typically you will create these models by annotating them with the @Observable
macro:
@Observable
class SearchModel {
var query: String = ""
var results: [SearchResult] = []
// ...
}
When you pass this model to a SwiftUI view, you might end up with something like this:
struct SearchView {
let searchModel: SearchModel
var body: some View {
TextField("Search query", text: // ...??)
}
}
Notice how the searchModel
is defined as a plain let
. We don't need to use @ObservedObject
when a SwiftUI view receives an Observable
model from one of its parent views. We also shouldn't be using @State
because @State
should only be used for model data that is owned by the view. Since we're passed our SearchModel
by a parent view, that means we don't own the data source and we shouldn't use @State
. Even without adding a property wrapper, the Observable
model is able to tell the SwiftUI view when one of its properties has changed. How this works is a topic for a different post; your key takeaway for now is that you don't need to annotate your Observable
with any property wrappers to have your view observe it.
Back to SearchView
. In the SearchView
body we create a TextField
and this TextField
needs to have a binding to a string value. If we'd be working with an @ObservedObject
or if we owned the SearchModel
and defined its proeprty as @State
we would write $searchModel.query
to obtain a binding.
When we attempt to do this for our current searchModel
property now, we'd see the following error:
var body: some View {
// Cannot find '$searchModel' in scope
TextField("Search query", text: $searchModel.query)
}
Because we don't have a property wrapper to create a projected value for our search model, we can't use the $
prefix to create a binding.
To learn more about property wrappers and projected values, read this post.
In order to fix this, we need to annotate our searchModel
with @Bindable
:
struct SearchView {
@Bindable var searchModel: SearchModel
var body: some View {
TextField("Search query", text: $searchModel.query)
}
}
By applying the @Bindable
property wrapper to the searchModel
property, we gain access to the $searchModel
property because the Bindable
property wrapper can now provide a projected value in the form of a Binding
.
Note that you only need the @Bindable
property wrapper if:
- You didn't create the model with
@State
(because you can create bindings to@State
properties already) - You need to pass a binding to a property on your
Observable
model
Essentially, you will only need to use @Bindable
if in your view you write $myModel.property
and the compiler tells you it can't find $myModel
. That's a good indicator that you're trying to create a binding to something that can't provide a binding out of the box, and thay you'll want to use @Bindable
to be able to create bindings to your model.
Hopefully this post helps clear the purpose and usage of @Bindable
up a little bit!