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

@Binding made easy
@Binding made easy

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.