Understanding `@State` in SwiftUI: How It Works Under the Hood



This content originally appeared on DEV Community and was authored by Karthik Pala

One of the most magical things about SwiftUI is how little code you need to keep your UI in sync with your data. Declare a property with @State, mutate it, and your view just updates. Simple, right?

But if you’ve ever wondered what’s really happening when you use @State, this article takes a peek under the hood.

Why Do We Even Need @State?

Lets take below example

struct CounterView: View {
    private var count = 0

    var body: some View {
        Button("Tap \(count)") {
            count += 1
        }
    }
}

The above code looks good right? Create a button that says “Tap Count” plus the number of times the button has been tapped, then add 1 to tapCount whenever the button is tapped

But this doesn’t compile because CounterView is a struct and you can’t change properties freely. Okay lets keep this aside for a minute, What other problems do we have with above code?

SwiftUI views get created and destroyed all the time as SwiftUI updates the UI tree.

If count were just a plain Int property on a struct, it would reset to 0 every time the view was recreated. We’d lose our state instantly.

This is where @State comes in. It tells SwiftUI:

“Hey, this piece of data should survive across view reloads.”

For example:

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

    var body: some View {
        Button("Tap \(count)") {
            count += 1
        }
    }
}

What @State Really Is

@State is a property wrapper provided by SwiftUI. When you write:

@State private var count = 0

Under the hood, the State type looks like this (simplified assumption):

@propertyWrapper
struct State<Value>: DynamicProperty {
    init(initialValue: Value)
    var wrappedValue: Value { get nonmutating set }
    var projectedValue: Binding<Value> { get }
}

So, count is just wrappedValue inside a property wrapper. But the important part is where that value is stored.

Notice the nonmutating set on wrappedValue — this is what allows us to change a @State variable inside a struct View even though structs are normally immutable in SwiftUI’s body.

Note: See the section on DynamicProperty below to understand how it helps the State property wrapper

Where Does the Value Live?

When you set @State var count = 0, the value 0 is not stored inside your view struct

Instead, SwiftUI maintains a hidden state storage that lives outside the view. Each piece of state is associated with the identity of the view in the UI hierarchy.

Think of it like this:

Every time SwiftUI re-creates CounterView, it reattaches _count to the same storage box. That’s why the state survives view updates.

How Updates Trigger UI Refresh

When you mutate a @State variable:

count += 1

The setter does two things:

  1. Updates the underlying stored value in SwiftUI’s state system.
  2. Marks the view as dirty and schedules a re-render.

That’s why the UI automatically refreshes with the new value — SwiftUI invalidates the current body and recomputes it.

@State and Binding

When you use the $ prefix, you don’t get the raw value — you get a Binding:

$count  // Binding<Int>

A Binding is essentially a lightweight reference to the same state storage. This allows child views to read and write the parent’s state without owning it.

Example:

TextField("Name", text: $name)

Here, TextField can directly mutate the name state in the parent view.

Lifecycle of a @State Property

At runtime, the flow looks like this:

  1. SwiftUI builds the view struct (CounterView).
  2. It sees a @State property and checks if there’s existing storage.
  3. If storage exists, it reuses it; otherwise, it creates a new storage box.
  4. The view’s body runs, reading count via wrappedValue.
  5. If you change count, SwiftUI updates storage and schedules the body to recompute.

A Toy Implementation of @State

SwiftUI is a closed-source Apple framework, but because property wrappers in Swift are a public language feature, we can only imagine and re-implement how @State could work.

Of course, the real @State is tightly integrated with SwiftUI’s runtime. But we can mimic the idea with an example property wrapper:

@propertyWrapper
public struct State<Value>: DynamicProperty {
    private var storage: StateStorage<Value>

    public init(wrappedValue: Value) {
        self.storage = .init(initialValue: wrappedValue)
    }

    public var wrappedValue: Value {
        get { storage.value }
        nonmutating set { storage.value = newValue }
    }

    public var projectedValue: Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { self.wrappedValue = $0 }
        )
    }
}

This obviously doesn’t persist across view recreations, but it shows the principle:

  • wrappedValue gives you the value.
  • Mutating it triggers a refresh.
  • $property gives you a Binding.

The above example is inspired from the State property wrapper implementation in OpenSwiftUI project here

What’s DynamicProperty Doing Here?

In SwiftUI, some property wrappers (like @State, @ObservedObject, @EnvironmentObject, @AppStorage, etc.) conform to the DynamicProperty protocol.

public protocol DynamicProperty {
    mutating func update()
}

This protocol lets SwiftUI know:

  1. This property participates in the SwiftUI data flow.

    • SwiftUI calls update() before recomputing the view’s body.
    • That’s how the property wrapper gets a chance to reconnect to the right storage or refresh its bindings.
  2. Why it matters for @State:

    • Every time SwiftUI re-renders a view, a new struct instance of your view is created.
    • Without DynamicProperty, the State wrapper would just get re-initialized, losing its data.
    • With DynamicProperty, SwiftUI ensures the wrapper reattaches to its persistent state storage instead of resetting.

Understanding how @State works demystifies a lot of SwiftUI. It’s not magic — it’s just clever indirection and lifecycle management. By keeping state outside the view struct, SwiftUI ensures your UI is always a pure function of its data, while your data itself remains persistent.

So the next time you write @State var count = 0, remember: you’re really just getting a little persistent box managed by SwiftUI, with automatic UI updates wired in.

References

1) Opensource implementation of SwiftUI – https://github.com/OpenSwiftUIProject/OpenSwiftUI
2) https://forums.swift.org/t/how-does-swiftui-find-state-properties-in-views/79984/3
3) https://fatbobman.com/en/posts/swiftui-state/


This content originally appeared on DEV Community and was authored by Karthik Pala