Back to blog

Working with @Binding in SwiftUI


Aasif Khan
By Aasif Khan | Last Updated on December 25th, 2023 3:14 pm | 4-min read

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 mobile app development 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

Describe your app idea
and AI will build your App

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.

Furthermore, @Binding plays a pivotal role in maintaining the state and data flow in SwiftUI. It allows for a decoupled architecture where views can remain agnostic of the data source but still reflect the latest data. This is particularly useful in scenarios where you have nested views or components, and you want to ensure that they all reflect the same piece of data.

By using @Binding, you’re not duplicating the data but referencing the same source of truth, ensuring consistency across your app. Another advantage is the reduction in memory usage, as multiple views can rely on a single data source without unnecessary replication.

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, where Value is the type of the data you’re binding.

For example, @Binding var isPlaying: Bool will create a binding of type Binding. 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.

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)
}
}

Having observed the example, it’s evident how @Binding and @State collaborate for effective state management in SwiftUI. The @State property wrapper is localized, primarily used for state within a single view, while @Binding acts as a reference to that state, facilitating its sharing or manipulation by other views. This dynamic ensures data consistency across the UI hierarchy. Such interconnectedness ensures that changes in one view are seamlessly propagated to other related views, fostering a synchronized and reactive UI.

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†.

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, and the type of the count parameter for CountButton’s initializer is Binding. 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.

The seamless integration of @Binding with various UI components in SwiftUI showcases the framework’s commitment to reactive programming. This reactive nature ensures that the UI remains in sync with the underlying data, providing users with a responsive and intuitive experience.

The use of bindings in interactive views, such as Toggle, TextField, and Picker, eliminates the need for manual event handling or data synchronization, simplifying the development process. This not only reduces potential bugs but also allows developers to focus on crafting engaging user experiences. Furthermore, the flexibility of SwiftUI’s design means that developers aren’t limited to the built-in UI components.

By understanding and leveraging the power of @Binding, developers can create custom UI components that adhere to the same reactive principles, ensuring consistency and reusability across the app.

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. 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.

The beauty of this mechanism lies in its simplicity and efficiency. By passing bindings to interactive views, you’re essentially creating a dynamic link between the data and the UI component. This not only ensures real-time updates but also reduces the need for additional code to manage these updates.

In more complex applications, this can be a game-changer, as it streamlines the data flow and reduces potential points of failure. Moreover, by leveraging bindings in this manner, developers can maintain a clean and modular codebase, where each view is responsible for its own state, yet can easily interact with the states of other views.

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

Conclusion

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.

Aasif Khan

Head of SEO at Appy Pie

App Builder

Most Popular Posts