@State and @Binding
SwiftUI's declarative model requires a clear answer to the question 'who owns this piece of data?' @State answers that for simple, view-local values: it marks a property as owned and persisted by a specific view instance, surviving across body re-evaluations even though the struct itself is recreated each render. @Binding, in contrast, marks a property as *not* owned by the current view—it's a reference to state that lives somewhere else (a parent view's @State, or a model object), passed down so a child can read and mutate it without becoming the source of truth itself. Together they let you compose reusable child views that manipulate a parent's data safely.
Cricket analogy: The team's official scorer (a @State-holding view) owns and persists the running total across every over even as commentary changes; a substitute fielder brought on briefly (a @Binding-holding child view) doesn't own the score, just reads and can appeal to affect it via the real scorer.
How @State Works Under the Hood
A SwiftUI View is a value type (usually a struct), and its body is recomputed frequently—every re-render creates a new struct instance. If a plain var held your data, it would reset every time. @State solves this by storing the actual value outside the struct, in storage managed by SwiftUI's rendering engine, keyed to that specific instance's identity in the view tree. The property wrapper gives you a projected value ($property) that produces a Binding, and reading property returns the current stored value. @State should almost always be private, since it represents data owned exclusively by that view.
Cricket analogy: A scorecard printed fresh for every over would forget the running total if it just used a blank sheet each time; @State instead keeps the actual tally in a separate ledger tied to that specific match, so a new scorecard printout still shows the correct persisted score, and that ledger should be for the scorer's eyes only ('private'), like a chess player's clock — not shared beyond the innings.
struct FavoriteToggle: View {
@State private var isFavorited = false
var body: some View {
Button {
isFavorited.toggle()
} label: {
Image(systemName: isFavorited ? "star.fill" : "star")
.foregroundStyle(isFavorited ? .yellow : .gray)
}
}
}Passing State Down with @Binding
When a child view needs to modify a parent's @State, you don't pass the raw value—you pass a Binding using the $ prefix. The child declares the parameter with @Binding var someValue: T, and any mutation the child makes (someValue = newValue) writes straight through to the parent's storage. This is the mechanism behind reusable controls: a custom RatingControl component, for example, can be dropped into any screen and wired to whatever @State the caller owns, without RatingControl needing to know or care where that state ultimately lives.
Cricket analogy: A reusable "run-rate widget" component doesn't need to know which match it belongs to — the app passes it a Binding to that match's actual score via $score, so when the widget's user taps to correct a run, it writes straight through to the real scorecard, no matter which ground is hosting the game.
struct RatingControl: View {
@Binding var rating: Int
let maximum = 5
var body: some View {
HStack {
ForEach(1...maximum, id: \.self) { star in
Image(systemName: star <= rating ? "star.fill" : "star")
.onTapGesture { rating = star }
}
}
}
}
struct ReviewScreen: View {
@State private var rating = 0
var body: some View {
RatingControl(rating: $rating)
}
}A useful mental model: @State is the well, @Binding is the bucket-and-rope. The well (parent) actually holds the water (data); the rope (Binding) lets someone standing elsewhere (the child view) draw from and pour into that same well, but they never own a separate well of their own.
Never mark @State as non-private and pass the raw property to a child expecting mutation—Swift won't even compile that for a @Binding parameter, but it's tempting to instead duplicate the value into a new @State in the child, which silently breaks the two-way link and causes UI desync bugs.
Constant Bindings for Previews
For SwiftUI previews or read-only contexts, Binding.constant(value) creates a Binding that ignores writes, which is handy for previewing a child view that expects a @Binding without wiring up a full parent.
Cricket analogy: Setting up a practice net session with a "dead ball" that a coach uses purely to demonstrate batting stance without it actually counting toward any score is like Binding.constant — useful for previewing technique without wiring up a real match.
- @State stores view-local data outside the struct, so it survives body re-evaluations for that view's identity.
- @State properties should be declared private since they represent data the view exclusively owns.
- @Binding represents a reference to state owned elsewhere; it never creates its own storage.
- Use the $ prefix on a @State property to produce a Binding to pass to a child view.
- Mutating a @Binding writes through to the original @State, keeping parent and child in sync.
- Binding.constant(_:) is useful for previews of views that require a Binding parameter.
Practice what you learned
1. Why can't a plain `var` property hold persistent UI state in a SwiftUI View struct?
2. What does the `$` prefix on a @State property produce?
3. Which access-control convention is recommended for @State properties?
4. What happens if a child view declares its own @State instead of accepting a @Binding for a value it should mutate on behalf of a parent?
5. What is Binding.constant(value) typically used for?
Was this page helpful?
You May Also Like
ObservableObject and @ObservedObject
Learn how the pre-iOS 17 ObservableObject protocol and @Published/@ObservedObject let reference-type models publish changes that drive SwiftUI view updates.
The @Observable Macro
See how the iOS 17 @Observable macro replaces ObservableObject with automatic, fine-grained property tracking that simplifies SwiftUI model code.
Buttons and User Input in SwiftUI
Learn how SwiftUI captures user intent through Button, Toggle, Slider, Stepper, and TextField, and how actions drive state changes in a declarative UI.
Passing Data Between Views
A practical guide to the different ways data flows through a SwiftUI view hierarchy — initializers, @Binding, environment, and shared observable models.