How to use @State property wrapper in SwiftUI | SwiftUI Bootcamp #19
@State is the foundation of interactivity in SwiftUI — it's how a view owns and responds to its own mutable data. After this lesson you'll understand what makes @State special, how it survives view re-renders, and when to use it versus other property wrappers.
What You'll Learn
- What
@Statedoes, how it stores data outside the view struct, and why that matters - The reactive loop: user action → state mutation → view re-render
- When
@Stateis the right choice and when to reach for@Binding,@ObservedObject, or@EnvironmentObject
Mental Model
Think of @State like a sticky note attached to your view — SwiftUI re-draws the view whenever the note changes, but the note belongs only to that view and nobody else can scribble on it directly. If you need a child view to change the note, you pass it a copy of the pen (@Binding) but the note still lives with the parent.
Here's the subtlety: SwiftUI view structs are recreated very frequently (on every render pass). A regular var defined inside the struct would lose its value every time the struct is recreated. @State solves this by storing the actual value outside the struct in SwiftUI's managed memory — the struct is recreated, but it reconnects to the same persistent value each time. The sticky note survives even when the wall is repainted.
Detailed Explanation
When you mark a property @State var count: Int = 0, SwiftUI does three things: (1) allocates persistent storage for the value outside the struct, (2) synthesizes a getter and setter for it, and (3) marks the view as needing a re-render any time the value changes via the setter.
Because the storage lives outside the struct, the value persists across re-renders. This is why you can tap a button 10 times and see the count increment correctly, even though the view struct is recreated on each render.
@State is intended for local, view-private state — things the view itself manages and no other view needs to know about. Good examples: whether a sheet is showing, the current text in a search field, the selected tab, a loading flag. Poor candidates for @State: data that needs to be shared between sibling views, data that comes from a network call, data that should persist to disk.
When state needs to be shared, you pass it down to child views either as a @Binding (for read/write access) or as a plain let value (for read-only access). The rule of thumb: a piece of state should live in the lowest common ancestor of all the views that need it.
Code Structure
The sample is in StateBootcamp.swift and drives three separate state variables: backgroundColor (a Color), myTitle (a String), and count (an Int). Two buttons update all three values simultaneously, and the entire screen (background, title text, and count label) reflects the current state — demonstrating that one button press can trigger a coordinated multi-view update.
Complete Code
StateBootcamp.swift
import SwiftUI
struct StateBootcamp: View {
@State var backgroundColor: Color = Color.green // owns the current background color
@State var myTitle: String = "My Title" // owns the current title string
@State var count: Int = 0 // owns the current count value
var body: some View {
ZStack {
// background
backgroundColor // fills the entire screen with the current @State color
.edgesIgnoringSafeArea(.all) // extends the color behind the status bar and home indicator
// content
VStack(spacing: 20) {
Text(myTitle) // re-renders automatically whenever myTitle changes
.font(.title)
Text("Count: \(count)") // string interpolation reads the current count on every render
.font(.headline)
.underline()
HStack(spacing: 20) {
Button("BUTTON 1") {
backgroundColor = .red // mutates @State — triggers full re-render
myTitle = "BUTTON 1 was pressed"
count += 1 // increments count — the previous value persists between renders
}
Button("BUTTON 2") {
backgroundColor = .purple
myTitle = "BUTTON 2 was pressed"
count -= 1
}
}
}
.foregroundColor(.white) // makes all text white so it's readable on the colored background
}
}
}
struct StateBootcamp_Previews: PreviewProvider {
static var previews: some View {
StateBootcamp()
}
}Code Walkthrough
- Three
@Statevariables — Each one is independently tracked by SwiftUI. Mutating any of them triggers a re-render. They're initialized with default values that define the screen's starting state. ZStackwith background color as first child —backgroundColoris used directly as a view (sinceColorconforms toView). As the first item in theZStack, it's rendered below everything else..edgesIgnoringSafeArea(.all)extends it to fill the entire screen including safe area regions.Text(myTitle)andText("Count: \(count)")— These are reactive: they read@Statevalues. WhenmyTitleorcountchanges, SwiftUI re-evaluatesbody, which re-evaluates theseTextexpressions with the new values and updates the displayed text automatically.Button("BUTTON 1") { ... }action closure — Mutates all three state variables in a single action. SwiftUI batches these mutations and applies one re-render at the end of the closure, not three separate re-renders. This is an important optimization — you don't need to worry about intermediate render states.count += 1vs.count = count + 1— Identical behavior.count += 1is shorthand. The important thing is that mutatingcountthrough the@Statesetter is what triggers the update. Directly setting the raw stored value (which you can't do from outside@State) would not trigger a re-render..foregroundColor(.white)onVStack— Applies to allTextand symbolic views inside theVStackvia inheritance. This is more efficient than applying.foregroundColor(.white)to eachTextindividually.
Common Mistakes
Mistake: Declaring @State as a let constantlet properties are immutable and can't be the @State storage. Always declare @State properties with var. The Swift compiler will catch this, but it's a common typo when first learning.
Mistake: Using @State for data that needs to be shared with sibling views@State is private to the view that owns it. If a parent view and a sibling both need to react to the same value, don't use @State in a child and pass it up — instead, lift the state up to the parent (or to a shared ObservableObject model) and pass it down via @Binding or the environment.
Mistake: Marking a large model object as @State@State is designed for small, value-type properties: Int, Bool, String, Color, simple custom structs. For complex reference-type models with multiple published properties, use @StateObject (to own the lifecycle) or @ObservedObject (to observe an externally owned object). Using @State for a large class doesn't trigger property-level updates correctly.
Key Takeaways
@Statestores its value outside the view struct so it persists across re-renders — regularvarproperties reset on every render- Mutating a
@Statevariable inside a button action triggers SwiftUI's reactive re-render loop automatically - Use
@Statefor local, view-private state; lift state to a parent view or a shared model when multiple views need the same data
Last updated: June 27, 2026