@Binding made easy
Learn how @Binding lets SwiftUI views share and update state seamlessly. We’ll cover the difference between read-only and read & write bindings, with clear examples to keep your code clean and in sync.
Introduction
If you’ve read my previous post on @State, you already know how it helps a view own and manage its own data.
But what happens when a child view needs to read and update data that’s stored in its parent view? That’s where @Binding comes in. ✨
Think of @Binding as a bridge:
- The parent view keeps the actual data (with @State).
- The child view uses @Binding to access and modify that data.
This allows multiple views to stay in sync, without duplicating the source of truth.
In short: @State owns the data, @Binding shares it.
Why @Binding Exists
When you use @State, the data is owned by a single view. This works perfectly if only that view needs to read and update the value.
But in real apps, things get more complex. Often, a parent view manages some data, while a child view needs to display or change it.
If we passed the value without @Binding, the child would only get a copy. Updating it inside the child wouldn’t affect the original data in the parent.
That’s where @Binding comes in:
- It creates a two-way connection between parent and child.
- The parent still owns the data (with @State).
- The child reads and writes through the @Binding.
One-Way vs Two-Way Binding
When a parent view needs to share data with its child views, there are two ways to do it:
- One-way (read-only): The child view can see the value, but cannot change it.
- Two-way (read & write): The child view can read and update the value, keeping both views in sync.
Let’s see how @Binding works in practice. In this example:
- The parent view owns the state using @State.
- One child view (
CounterControls) uses @Binding to read and update that state. - Another child view (
CounterDisplay) receives the value read-only and cannot modify it.
struct CounterView: View {
@State private var count = 0
var body: some View {
VStack(spacing: 20) {
// Read-only child
CounterDisplay(count: count)
// Read & write child
CounterControls(count: $count)
}
.padding()
}
}
Here, CounterView owns the state and passes it down to each child:
count→ read-only$count→ binding for read & write
One-Way Binding (Read-Only)
CounterDisplay can see the value, but cannot change it.
struct CounterDisplay: View {
let count: Int
var body: some View {
Text("Current count: \(count)")
.font(.title)
}
}
Two-Way Binding (Read & Write)
CounterControls can read and modify the same state the parent owns.
struct CounterControls: View {
@Binding var count: Int
var body: some View {
HStack(spacing: 20) {
Button("Increase") { count += 1 }
Button("Decrease") { count -= 1 }
}
}
}
How It Works
- The parent view is the single source of truth, holding the data with @State.
- The $ symbol creates a two-way binding when passing the value down.
- The read-only child uses a plain property (let) to observe the value.
- The read & write child uses @Binding to update the original state directly.
This pattern keeps your data centralized and consistent, while letting each child view play its role.
Common Pitfalls
Forgetting the $
When passing a binding to a child view, you must use $ before the variable.
CounterControls(count: count) // Missing $
Using @Binding in the Parent View
The parent view should always own the state with @State. Only the child view should use @Binding.
struct CounterView: View {
@Binding var count: Int // ❌ Wrong place for @Binding
}
Recap
- @State is used in the parent view to own and manage data.
- @Binding is used in the child view to read and update that data.
- $ is what creates the connection when passing the state down.
By combining @State and @Binding, you keep your data centralized, your views synchronized, and your code clean and predictable.