Working with @Binding in SwiftUI

Written by LearnAppMaking on February 26 2021 in App Development, iOS, SwiftUI

Working with @Binding in SwiftUI

A binding in SwiftUI is a connection between a value and a view that displays and changes it. You can create your own bindings with the @Binding property wrapper, and pass bindings into views that require them. In this tutorial, we’re going to discuss how bindings work in SwiftUI.

Here’s what we’ll get into:

  • What’s a binding and why do you need them?
  • The difference between a binding and @Binding
  • How to get a binding from other property wrappers, like @State
  • Which property wrappers provide bindings, and when to use them
  • How to provide a binding to a UI element like TextField

Ready? Let’s go.

  1. What’s @Binding in SwiftUI?
  2. Working with @Binding and @State
  3. Passing Bindings to Interactive Views
  4. Property Wrappers with a Binding
  5. Further Reading

What’s @Binding in SwiftUI?

A binding is essentially a connection between a value, like a Bool, and an object or UI element that displays and changes it, like a slider or text field. The connection is two-way, so the UI element can change the value, and the value can change the UI element.

In SwiftUI, you can create bindings in 2 ways:

  1. With the @Binding property wrapper, which creates a binding, but doesn’t store it
  2. With other property wrappers, like @State, which creates a binding, and also stores its value

Based on the first approach, we can assert that you can use the @Binding property wrapper in SwiftUI to connect to some state stored elsewhere. An example is @State, whose binding you can pass to a property marked with @Binding. This is a common (if not most common) use case for bindings in SwiftUI!

Before we move on, let’s discuss bindings, the concept, vs. @Binding, the property wrapper. They’re both similar but distinct concepts, so it’s important we get that part of the playing field clear before moving forward.

Bindings

A binding is, like its name implies, a connection between 2 things. In SwiftUI, a binding sits between a property that stores data, and a view that displays and changes that data.

Bindings have been a part of reactive programming long before SwiftUI (and iOS) adopted it. React, the JavaScript framework, uses bindings, and so did Angular before it. In fact, event listeners, observers and even good ol’ Notification Center uses a form of connection or “binding” to accomodate the flow of data.

Working with bindings in SwiftUI is super concise – it only takes a $ sign – and that’s an advantage over less modern approaches like Notification Center. Bindings are almost magical: you connect one thing to the other, and data flows between them when changes happen.

@Binding

The @Binding keyword in SwiftUI is a property wrapper, which means that it wraps a property with some extra functionality. In the case of @Binding, it’s synthesizing a value of type Binding<Value>, where Value is the type of the data you’re binding.

For example, @Binding var isPlaying: Bool will create a binding of type Binding<Bool>. This value of type Binding, the binding “itself”, can be passed around via the projected value of the property, with $isPlaying. This allows you to read/write the value of the property with isPlaying, as well as pass around a binding to it with $isPlaying.

It’s hard to see property wrappers and bindings as separate concepts, because they’re so closely related. It’s good to know that property wrappers like @ObservedObject also expose bindings, which means that some property wrappers will use/expose a binding, and others will expose something else.

Let’s move on!

Working with @Binding and @State

Let’s take a look at an example. First, we’re creating a CountButton view:

struct CountButton: View
{
    @Binding var count: Int

    var body: some View {
        Button("\(count) times") {
            count += 1
        }
    }
}

Next, we’re creating a CounterView:

struct CounterView: View
{
    @State private var swiftCount: Int = 0

    var body: some View {
        Text("Times spotted a purple-and-gold-striped swift:")
        CountButton(count: $swiftCount)
    }
}

Let’s break down what’s going on in this code. First, check out how we’re using 2 property wrappers:

  1. @State private var swiftCount: Int = 0 keeps track of the count in CounterView
  2. @Binding var count: Int is a binding to an Int stored elsewhere

We’ve got 2 views in this code. The top-level view is CounterView, which has a Text view with a simple string value. The CounterView also includes CountButton as a subview.

Inside CountButton, you only see a Button subview. This button has 2 parameters:

  1. "\(count) times", the text on the button, using the value of count
  2. { count += 1 }, the action (a closure) that’s executed when you tap it

You’ve probably guessed it already at this point, but these views work together to increase the value of count when you tap the button. Eeeeasy!

What’s magical about the code is how @State and @Binding work together. See, inside CountButton we need access to the swiftCount property on CounterView because that’s where we display and change its value.

We only want to store the count once – a single source of truth – so we need some way to pass it into CountButton. That’s where the binding comes in.

Inside CountButton, the count property is marked with @Binding. This tells the view to update itself whenever the value of count changes. The @Binding property, however, does not store its own value. The state of count is stored elsewhere. Where, you ask? In the CountView struct, with the swiftCount property!

Because the count property in CountButton is marked with @Binding, and because it doesn’t have a default value, we can pass a binding into the CountButton(count:) initializer. The type of its count parameter is Binding<Int>.

The @State property wrapper exposes a binding to its underlying value, which we can get to via the $ prefix. For an @State property named swiftCount, the name of the binding is $swiftCount. This is effectively a second property that’s synthesized because of @State.

In short, this is a contrived and complicated way to pass some data into the CountButton view. But you can see how bindings and @Binding come in handy when the data you’re working with is more complex. You use @Binding to create/pass a binding to some state that’s created outside the view.

: Note that the type of the count property is Int, the type of $count and $swiftCount is Binding<Int>, and the type of the count parameter for CountButton’s initializer is Binding<Int>. Usually, the memberwise initializer has the same types as the properties it’s based on – but not for @Binding!

Note: It’s important to note that @Binding will make a view dependant on its state, but @Binding does not store its own state. It’ll always come from elsewhere, like @State or @ObservedObject. A good reminder of this design decision is that properties marked with @State are generally made private (“local state”) and properties marked with @Binding aren’t declared with a default value.

Passing Bindings to Interactive Views

Let’s take a look at another example of @Binding. In this case, the principles are the same as before – but the code is entirely different.

Check this out:

struct LivingRoom: View
{
    @State private var lightsOn = false

    var body: some View {
        Text(lightsOn ? "Lights on! 🌞" : "Lights off 🌚")
        Toggle("Control lights", isOn: $lightsOn)
    }
}

What’s going on here? We’ve got that @State property wrapper again, for the property lightsOn. Based on what we’ve discussed before, you know that the LivingRoom view now depends on the state of the lightsOn boolean.

In the Text view, we’re checking the value of lightsOn with the ternary conditional operator ?: and show a bit of text:

  • if lightsOn is true, show "Lights on! 🌞"
  • if lightsOn is false, show "Lights off 🌚"

When the value of lightsOn changes, the view will update itself. This is a core principle of SwiftUI; state drives the UI. Only because we’re using @State for that property!

What about Toggle and that binding? See, we’re passing $lightsOn into the Toggle view, for its isOn parameter. This binds the on-off functionality of the Toggle to the value for lightsOn – “on” is true and “off” is false.

What’s interesting is that this approach is almost exactly the same as what we’ve coded in the previous section with @Binding. This makes you wonder: does the Toggle view use @Binding internally, to accept a binding as one of its properties? You bet! Looking through the documentation, we see that the type of isOn is Binding<Bool>. And that’s exactly the type of $lightsOn!

You’ll find that many UI components in SwiftUI accept bindings as parameters, because it’s the defacto approach to connect views that display/change data with the properties that store them.

A few examples of UI elements that use bindings:

  • TextField, SecureField and TextEditor accept a String binding for text
  • NavigationLink accepts a Bool binding for isActive, to show/navigate the UI programmatically
  • Toggle accepts a Bool binding for isOn, for the state of the toggle
  • Picker accepts a binding for selection, indicating the currently selected value
  • DatePicker accepts a binding of type Date for selection, i.e. its currently selected date
  • Slider accepts a binding for value (floating-point), i.e. the value on the slider
  • Stepper accepts a binding for value, i.e. the value that’s stepped up/down (uses Strideable, so that’s pretty flexible!)
  • ColorPicker accepts a binding of type Color for selection, i.e. the color that’s selected in the picker

You can, of course, also create your own UI elements (or other components) that use bindings and @Binding. That’s the whole point!

How do you pass data and bindings around in your code? Learn more in this tutorial: How To: Pass Data Between Views with SwiftUI

Property Wrappers with a Binding

So far we’ve looked at what bindings are and how you use the @Binding property wrapper in your code. We’ve also looked at UI components that use bindings, and how you can get a $name binding from the @State property wrapper.

Let’s take a closer look at that last bit. Whenever you create some state with the @State property wrapper, you also get a binding for free. This binding enables you to connect some UI element, like a TextField, to the property. When the text in the text field changes, the value in the property changes too – and vice versa.

Check out this brief example:

struct BookEdit: View
{
    @State private var title: String = ""
    @State private var author: String = ""

    var body: some View {
        TextField("Title", text: $title)
        TextField("Author", text: $author)
        ···
        Text("\(title) by \(author)")
    }
}

In the above code, we’ve created a property title of type String. It’s wrapped by @State. In the TextField initializer, we’re passing $title for its text parameter. This is a binding to the text property of the text field.

When we type something in the text field, the value of title changes too. This change is shown in the Text view, at the bottom, albeit a bit plainly. The binding is 2-way: the text field changes the value of text, and any changes to text are displayed in the TextField as well.

Note that the name of the binding is $title, so the name of the wrapped property plus a $ character. This is the property wrapper’s projected value. Many property wrappers have a projected (and wrapped) value; not not every property wrapper’s projected value is a binding!

It’s clear that, in order to work with a binding, you’ll need to get a binding somewhere. Which property wrappers provide bindings?

Here’s a brief overview:

  • @Binding – provides binding to property, does not own state, usable for value types (and properties of reference types, that are value types!)
  • @State – provides binding to property, owns state, used for value types
  • @ObservedObject – provides bindings to properties of an observed object, does not own state, initialized elsewhere, used for reference types
  • @EnvironmentObject – provides bindings to properties of an environment object, does not own state, initialized elsewhere, passed via environmentObject(_:)
  • @StateObject – provides bindings to properties of a state object, owns state, initialized locally, used for reference types
  • There are others: @SceneStorage, @GestureState, @FocusedValue and @FocusedBinding

Which of these property wrappers you’ll end up using depends largely on the specific property wrapper you need, but it’s good to know that they provide bindings to their properties.

A good rule of thumb, now and in the future, is that a property wrapper that provides/manages state probably also has a binding. It makes sense: you need state to read/write a value, and you can use the binding to bind a UI element to that state.

Looking for an in-depth tutorial about an ObserableObject that uses bindings? Check this out: @ObservedObject and Friends in SwiftUI

Further Reading

The core principle of a binding is that it connects a value with a UI element or view that displays and changes that value. The binding is 2-way, which means the UI element can change the value, and the value can change the UI element. This also means that, when you’re using @Binding, the view becomes dependant on its state. You can create your own bindings, or get them from property wrappers like @State and ObservedObject. Awesome!

Want to learn more? Check out these resources:

LearnAppMaking

LearnAppMaking

At LearnAppMaking.com, app developers learn how to build and launch awesome iOS apps.