@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.

@State made easy
@State made easy

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.

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)
    }
}

Lifecycle

@State keeps its value between view updates, even though SwiftUI recreates the view’s struct many times.

However, if the view leaves the hierarchy and comes back, the state resets to its initial value.

struct ContentView: View {
    @State private var showCounter = false

    var body: some View {
        VStack {
            if showCounter {
                CounterView() // state lives here
            }

            Button(showCounter ? "Hide Counter" : "Show Counter") {
                showCounter.toggle()
            }
        }
    }
}

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

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increase") { count += 1 }
        }
        .padding()
    }
}

Tap Increase → count goes up and stays while CounterView is visible
Tap Hide Counter, then Show Counter CounterView is recreated, and count resets to 0

Wrapping up

@State is the simplest way to make your SwiftUI views dynamic and interactive.

It keeps local data alive while the view is on screen and automatically updates the UI when that data changes.

Start small, like counters, toggles, or text inputs, and you’ll quickly see how powerful this tiny property wrapper really is. 🚀