DEV Community

Karthik Pala
Karthik Pala

Posted on

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

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

What @State Really Is

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

@State private var count = 0
Enter fullscreen mode Exit fullscreen mode

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 }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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 }
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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/

Top comments (0)