@Binding lets a child view read and write a value that’s actually owned by a parent view, without the child needing to know where that value ultimately lives. It’s SwiftUI’s equivalent of Compose’s state hoisting pattern.
The Problem Without @Binding
A plain parameter is read-only. If a child view takes a Bool as a normal parameter, it can display it but can’t change it in a way the parent sees:
struct ToggleRow: View {
let isOn: Bool // read-only, can't be changed by this view
var body: some View {
Text(isOn ? "On" : "Off")
}
}
Passing a Two-Way Connection
struct ToggleRow: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Enabled", isOn: $isOn)
}
}
struct ParentView: View {
@State private var enabled = false
var body: some View {
ToggleRow(isOn: $enabled)
}
}
The $ prefix on $enabled is what creates the Binding<Bool> from the @State property. Now when ToggleRow flips the toggle, it’s mutating the parent’s enabled property directly — there’s no copy, and no manual callback to wire up.
Where the $ Prefix Comes From
Every property wrapper that can produce a binding exposes it via the $ prefix: @State private var name gives you both name (the value) and $name (a Binding<String> to it). This is the same mechanism used when you pass a @State value directly into a built-in control like TextField("Name", text: $name) — TextField itself takes a Binding under the hood.
Creating a Binding Without @State
You can also construct a Binding manually with a custom getter and setter — useful when you want a two-way connection to something that isn’t itself @State, like a computed transformation of another value:
let customBinding = Binding(
get: { celsius },
set: { celsius = $0 }
)
Quick Rule
Use @Binding whenever a reusable child view needs to modify a value that conceptually belongs to its parent — a toggle, a text field, a stepper. It keeps the child view generic and reusable, since it doesn’t need to know or care where the value is ultimately stored.