@State made easy

Discover how @State helps SwiftUI views manage their own data. A simple guide with clear examples to understand how state works and why it's essential for building dynamic UIs.

The problem

SwiftUI views are structs, which means they’re lightweight and disposable.

Every time something in your app changes, SwiftUI rebuilds parts of the UI from scratch.

Without @State, any data inside the view would reset to its initial value every time it redraws, making it impossible to keep track of things like a counter, toggle, or text input.

struct CounterView: View {
    private var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increase") {
                // This looks like it should increase the count
                // but the view is rebuilt and 'count' resets to 0
                count += 1
            }
        }
    }
}

What @State is

@State is a property wrapper in SwiftUI that allows a view to store and manage its own data over time.

Think of @State as a storage box living outside the view struct. The view only gets a reference to it.

By marking a property with @State, SwiftUI keeps that value outside the view’s struct, so it persists between updates. This way, the view can reactively update itself whenever that state changes.

A @State property must be initialized inside the view where it’s declared.

It’s marked as private to keep the state owned and controlled by that view only, preventing external code from changing it directly and causing unexpected UI behavior.

struct CounterView: View {
    @State private var count = 0 // initialized here

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increase") {
                count += 1 // This now updates the state properly
            }
        }
    }
}

Local & Private

@State is owned by a single view and acts as that view’s source of truth.

It can be shared with child views in two ways:

  • Read-only: Pass the value directly. The child can read it but not change it.
  • Read & write: Pass a binding ($state). The child can read and update the value. I’ll cover @Binding in a separate post for a deeper look at two-way data sharing.
struct SettingsView: View {
    @State private var isEnabled = false

    var body: some View {
        VStack {
            StatusIndicator(isEnabled: isEnabled)     // read-only
            NotificationsToggle(isEnabled: $isEnabled) // read & write
        }
    }
}

struct StatusIndicator: View {
    let isEnabled: Bool
    var body: some View {
        Text(isEnabled ? "Enabled" : "Disabled")
    }
}

struct NotificationsToggle: View {
    @Binding var isEnabled: Bool
    var body: some View {
        Toggle("Enable Notifications", isOn: $isEnabled)
    }
}

Recap

  • @State lets a view own and manage its own data
  • The value persists across view rebuilds
  • Mark it private to keep ownership clear
  • Pass the value for read-only access, or use $ for two-way binding

Enjoyed this post?

Subscribe to get new articles delivered to your inbox.

No spam. Unsubscribe anytime.