Skip to content

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 @State does, how it stores data outside the view struct, and why that matters
  • The reactive loop: user action → state mutation → view re-render
  • When @State is 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

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

  1. Three @State variables — 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.
  2. ZStack with background color as first childbackgroundColor is used directly as a view (since Color conforms to View). As the first item in the ZStack, it's rendered below everything else. .edgesIgnoringSafeArea(.all) extends it to fill the entire screen including safe area regions.
  3. Text(myTitle) and Text("Count: \(count)") — These are reactive: they read @State values. When myTitle or count changes, SwiftUI re-evaluates body, which re-evaluates these Text expressions with the new values and updates the displayed text automatically.
  4. 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.
  5. count += 1 vs. count = count + 1 — Identical behavior. count += 1 is shorthand. The important thing is that mutating count through the @State setter 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.
  6. .foregroundColor(.white) on VStack — Applies to all Text and symbolic views inside the VStack via inheritance. This is more efficient than applying .foregroundColor(.white) to each Text individually.

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

  • @State stores its value outside the view struct so it persists across re-renders — regular var properties reset on every render
  • Mutating a @State variable inside a button action triggers SwiftUI's reactive re-render loop automatically
  • Use @State for 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

Released under the MIT License.