What’s the difference between @Binding and @Bindable

Published on: June 10, 2023

With iOS 17, macOS Sonoma and the other OSses from this year's generation, Apple has made a couple of changes to how we work with data in SwiftUI. Mainly, Apple has introduced a Combine-free version of @ObservableObject and @StateObject which takes the shape of the @Observable macro which is part of a new package called Observation.

One interesting addition is the @Bindable property wrapper. This property wrapper co-exists with @Binding in SwiftUI, and they cooperate to allow developers to create bindings to properties of observable classes. So what's the role of each of these property wrappers? What makes them different from each other?

If you prefer learning by video, the key lessons from this blog post are also covered in this video:

To start, let's look at the @Binding property wrapper.

When we need a view to mutate data that is owned by another view, we create a binding. For example, our binding could look like this:

struct MyButton: View {
    @Binding var count: Int

    var body: some View {
        Button(action: {
            count += 1
        }, label: {
            Text("Increment")
        })
    }
}

The example isn' t particularly interesting or clever, but it illustrates how we can write a view that reads and mutates a counter that is owned external to this view.

Data ownership is a big topic in SwiftUI and its property wrappers can really help us understand who owns what. In the case of @Binding all we know is that some other view will provide us with the ability to read a count, and a means to mutate this counter.

Whenever a user taps on my MyButton, the counter increments and the view updates. This includes the view that originally owned and used that counter.

Bindings are used in out of the box components in SwiftUI quite often. For example, TextField takes a binding to a String property that your view owns. This allows the text field to read a value that your view owns, and the text field can also update the text value in response to the user's input.

So how does @Bindable fit in?

If you're famliilar with SwiftUI on iOS 16 and earlier you will know that you can create bindings to @State, @StateObject, @ObservedObject, and a couple more, similar, objects. On iOS 17 we have access to the @Observable macro which doesn't enable us to create bindings in the same way that the ObservableObject does. Instead, if our @Observable object is a class, we can ask our views to make that object bindable.

This means that we can mark a property that holds an Observable class instance with the @Bindable property wrapper, allowing us to create bindings to properties of our class instance. Without @Bindable, we can't do that:

@Observable
class MyCounter {
    var count = 0
}

struct ContentView: View {
    var counter: MyCounter = MyCounter()

    init() {
        print("initt")
    }

    var body: some View {
        VStack {
            Text("The counter is \(counter.count)")
            // Cannot find '$counter' in scope
            MyButton(count: $counter.count)
        }
        .padding()
    }
}

When we make the var counter property @Bindable, we can create a binding to the counter's count property:

@Observable
class MyCounter {
    var count = 0
}

struct ContentView: View {
    @Bindable var counter: MyCounter

    init() {
        print("initt")
    }

    var body: some View {
        VStack {
            Text("The counter is \(counter.count)")
            // This now compiles
            MyButton(count: $counter.count)
        }
        .padding()
    }
}

Note that if your view owns the Observable object, you will usually mark it with @State and create the object instance in your view. When your Observable object is marked as @State you are able to create bindings to the object's properties. This is thanks to your @State property wrapper annotation.

However, if your view does not own the Observable object, it wouldn't be appropriate to use @State. The @Bindable property wrapper was created to solve this situation and allows you to create bindings to the object's properties.

Usage of Bindable is limited to classes that conform to the Observable protocol. The easiest way to create an Observable conforming object is with the @Observable macro.

Conclusion

In this post, you learned that the key difference between @Binding and @Bindable is in what they do. The @Binding property wrapper indicates that some piece of state on your view is owned by another view and you have both read and write access to the underlying data.

The @Bindable property wrapper allows you to create bindings for properties that are owned by Observable classes. As mentioned earlier,@Bindable is limted to classes that conform to Observable and the easiest way to make Observable objects is the @Observable macro.

As you now know, these two property wrappers co-exist to enable powerful data sharing behaviors.

Cheers!

Categories

Swift

Subscribe to my newsletter