@State & @Binding
Reviewed & published by Brayan K
SwiftUI views are value types, so how can they change? The answer is property wrappers. @State gives a view its own mutable source of truth, and @Binding lets a child read and write state that a parent owns. Master these and your UIs come alive.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Building iOS Apps with SwiftUI • Intermediate
What You'll Learn in This Lesson
- • Declare local mutable state with @State
- • Understand why state changes refresh the body
- • Share writable state with a child using @Binding
- • Use the $ prefix to pass a binding
- • Know why value-type views still work with mutable state
1️⃣ @State — A View's Own Source of Truth
Mark a property with @State to give the view a small piece of mutable data it owns. SwiftUI stores that value for you (outside the struct) and watches it. When you change it, SwiftUI re-runs body and redraws. Keep it private — it belongs to this view alone.
import SwiftUI
// @State gives a view its OWN mutable value. SwiftUI stores it outside the
// struct and watches it: when it changes, SwiftUI re-runs body automatically.
struct CounterView: View {
// local source of truth — conventionally private
@State private var count = 0
var body: some View {
VStack {
Text("Count: \(count)")
Button("Increment") {
count += 1 // changing @State refreshes the view
}
}
}
}2️⃣ @Binding — Sharing Writable State
When a child view needs to read and write state owned by a parent, use @Binding. It's a two-way connection: the parent keeps the single source of truth, but the child can update it. You hand the child a binding using the $ prefix on the parent's @State.
3️⃣ The $ Prefix — Projected Values
Every property wrapper can expose a projected value via the $ prefix. For @State and @Binding, the projected value is a Binding. Use the plain name for the value, and $name when a control like TextField must write back.
import SwiftUI
// The $ prefix gives you the PROJECTED VALUE of a wrapped property —
// for @State and @Binding that projected value is a Binding.
struct NameEditor: View {
@State private var name = "Ada"
var body: some View {
VStack {
// 'name' -> the String value
Text("Hello, \(name)")
// '$name' -> a Binding<String> the TextField writes back into
TextField("Your name", text: $name)
}
}
}Your turn. Fill in the blanks to wire up local state and pass a binding to a child.
// 🎯 YOUR TURN — fill in each ___ then preview in Xcode.
import SwiftUI
struct LikeButton: View {
// 1) Declare local mutable state 'liked' starting at false
___ private var liked = false // 👉 the property wrapper for local state
var body: some View {
Button(liked ? "♥ Liked" : "♡ Like") {
liked.___() // 👉 method that flips a Bool
}
}
}
struct ParentView: View {
@State private var liked = false
var body: some View {
// 2) Pass 'liked' to a child as a binding using the right prefix
ChildLikeRow(liked: ___liked) // 👉 the projected-value prefix
}
}
struct ChildLikeRow: View {
@Binding var liked: Bool
var body: some View { Text(liked ? "Liked" : "Not yet") }
}Pro Tips
- 💡 One source of truth. Own the value with @State in one place; share it down with @Binding.
- 💡 Keep @State private. It's view-local; don't expose it.
- 💡 Remember the $. Pass a binding, not the raw value, when a child must write back.
Common Errors (and the fix)
- "Cannot convert value of type 'Bool' to expected type 'Binding<Bool>'" — you passed the value instead of a binding. Add the $ prefix: ToggleRow(isOn: $notifications).
- UI doesn't update after a change — the value isn't @State (or @Binding). A plain var won't trigger a refresh.
- "Cannot assign to property: 'self' is immutable" — you tried to mutate a plain stored property in body. Wrap it in @State.
Mini-Challenge: Stepper Widget
Build a parent view that owns an @State quantity starting at 1. Create a child QuantityStepper that takes a @Binding var quantity: Int and shows "−" and "+" buttons that decrease and increase it. Pass the state down with $, and show the live total in the parent.
🎉 Lesson Complete!
- ✅ @State = local mutable source of truth (keep it private)
- ✅ Changing @State re-runs body and updates the UI
- ✅ @Binding = a two-way reference to a parent's state
- ✅ The $ prefix turns state into a Binding
Practice quiz
What does the @State property wrapper provide?
- A shared global value
- Local mutable state owned by a view
- A database connection
- A constant value
Answer: Local mutable state owned by a view. @State gives a view its own piece of mutable state that SwiftUI stores and watches for changes.
What happens when an @State value changes?
- Nothing until you call refresh()
- SwiftUI re-computes the view's body
- The app crashes
- The value is discarded
Answer: SwiftUI re-computes the view's body. Changing @State invalidates the view, so SwiftUI re-runs body and updates the screen.
Why is @State usually marked private?
- It is required by the compiler
- Because it is local source of truth owned by that view
- To make it global
- It cannot be private
Answer: Because it is local source of truth owned by that view. @State is the view's own source of truth, so it's conventionally private to that view.
What does @Binding create?
- A copy of a value
- A constant
- A new source of truth
- A two-way connection to state owned elsewhere
Answer: A two-way connection to state owned elsewhere. @Binding is a read-write reference to state that lives in a parent view, enabling two-way data flow.
How do you pass an @State value as a binding to a child?
- By writing state.binding
- By prefixing the name with a dollar sign, like $name
- By using @Binding twice
- You cannot pass it
Answer: By prefixing the name with a dollar sign, like $name. The $ prefix (projected value) turns an @State into a Binding you can hand to a child view.
Inside a child, what does the plain binding name (without $) give you?
- The current underlying value
- The Binding wrapper itself
- Always nil
- A copy that won't update
Answer: The current underlying value. Reading a @Binding by its name yields the underlying value; writing to it updates the parent's state.
Why are SwiftUI views value types (structs)?
- So they can be subclassed
- To allow inheritance
- So SwiftUI can cheaply create and diff them
- Because classes are banned
Answer: So SwiftUI can cheaply create and diff them. Views are lightweight structs, so SwiftUI can recreate and compare them efficiently every update.
Since a view is a struct, why can its body still 'mutate' a counter?
- Structs are secretly classes
- @State stores the value outside the struct so SwiftUI manages it
- It does not actually change
- Swift copies the whole view
Answer: @State stores the value outside the struct so SwiftUI manages it. @State stores its value in storage managed by SwiftUI, not in the struct itself, so updates persist across recreations.
Which declaration is correct for a counter starting at 0?
- @State var count = 0
- @Binding var count = 0
- @State let count = 0
- var @State count = 0
Answer: @State var count = 0. @State var count = 0 declares mutable local state with an initial value.
When should you use @Binding instead of @State?
- Always, they are identical
- When the child needs to read AND change state owned by a parent
- Only for constants
- Never in SwiftUI
Answer: When the child needs to read AND change state owned by a parent. @Binding is for a child view that must read and write state whose source of truth lives in a parent.
Continue this course
- Previous: SwiftUI Views & the body Property
- Next: @StateObject & ObservableObject — Reference-type view models with @Published, @StateObject vs @ObservedObject
Frequently asked questions
What is @State?
@State is a property wrapper that gives a view its own local, mutable source of truth. SwiftUI stores the value outside the view struct and watches it; whenever it changes, SwiftUI re-computes the view's body and updates the screen. It's ideal for small, view-local data like a counter, a toggle, or text being typed.
Why is @State usually private?
Because @State is the view's own source of truth — it is owned by that single view and shouldn't be set from outside. Marking it private documents that ownership and prevents other code from mutating it directly. To let a child change it, you pass a Binding instead.
What is @Binding and how is it different from @State?
@Binding is a two-way reference to state that is owned elsewhere (usually a parent's @State). The child reads and writes through the binding, but the parent remains the single source of truth. @State creates and owns state; @Binding borrows it. Use @Binding when a reusable child view must mutate a parent's value.
What does the $ prefix do?
The $ prefix exposes a property wrapper's projected value. For @State and @Binding, that projected value is a Binding. So $count turns an @State Int into a Binding<Int> you can pass to a child view or to a control like TextField or Toggle that needs to write the value back.
If a view is a struct (a value type), how can it change?
The view struct itself is immutable and gets recreated often. @State works around this by storing its value in storage that SwiftUI manages, not inside the struct. So mutating an @State property updates that external storage and survives the struct being recreated — that's why SwiftUI can re-run body with the new value.