SwiftUI’s Bindable property wrapper explained

Published on: June 30, 2023

WIth 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!

Categories

Quick Tip SwiftUI

Subscribe to my newsletter