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:
- Updates the underlying stored value in SwiftUI’s state system.
- 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:
- SwiftUI builds the view struct (
CounterView
). - It sees a
@State
property and checks if there’s existing storage. - If storage exists, it reuses it; otherwise, it creates a new storage box.
- The view’s body runs, reading
count
viawrappedValue
. - 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 aBinding
.
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:
-
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.
- SwiftUI calls
-
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)