SwiftUI @State Is for View-Local Memory, Not App-Wide Truth
Use @State for temporary UI state inside one SwiftUI view. Learn why it resets, why background updates glitch, and when to switch to @StateObject.
16 Oct 2025, 08:51 UTC

A text field that forgets its own text
You build a SwiftUI form with a text field and a toggle. The user types an email address, taps a different row, and comes back. The field is empty. Or worse, you kick off a network request from a button, update a @State flag on a background queue when it finishes, and the UI flickers or never reflects the result.
The useful takeaway: @State is SwiftUI's mechanism for view-local, transient memory. It belongs to one view struct, it lives only as long as that view instance lives, and it must be touched on the main actor. If you need state that survives view recreation or is shared across views, @State is the wrong tool.
What @State actually owns
When you declare @State private var count = 0 inside a View struct, SwiftUI synthesizes hidden backing storage tied to that specific view instance. Changing the property marks the view as dirty, and SwiftUI re-evaluates its body on the next main-thread run loop. This is declarative state: you describe what the UI should look like for a given value, and SwiftUI updates the view hierarchy when the value changes.
Key boundaries:
- @State is valid only inside the view struct that declares it. Passing the binding to a child view is fine; storing it outside the struct breaks SwiftUI's ownership model.
- It works with value types such as Int, String, Bool, or structs. It is not designed for class instances that need stable identity.
- SwiftUI may recreate a view more often than you expect. When that happens, @State reverts to its initial value.
Worked example: a tap counter and a background mistake
Create a new SwiftUI file in Xcode (File → New → File → SwiftUI View). No special permissions are required beyond a standard iOS project targeting iOS 13 or later.
import SwiftUI
struct CounterView: View {
@State private var taps = 0
var body: some View {
VStack(spacing: 16) {
Text("Taps: \(taps)")
Button("Tap me") {
taps += 1
}
}
}
}
Run this in the Xcode Simulator. Each button tap increments taps, SwiftUI re-evaluates body, and the Text updates. That is the happy path.
Now introduce a background update. Replace the button action with this:
Button("Tap me") {
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 0.5)
taps += 1
}
}
The UI may not update, or you may see a glitch, because taps was mutated off the main actor. SwiftUI expects state changes to happen on the main thread so it can schedule body re-evaluation safely. Fix it by dispatching back:
Button("Tap me") {
DispatchQueue.global().async {
Thread.sleep(forTimeInterval: 0.5)
DispatchQueue.main.async {
taps += 1
}
}
}
Expected check: after the fix, the counter increments reliably half a second after tapping. Risk: if you forget the main dispatch, behavior can be inconsistent across iOS versions and device load.
When @State resets, and what to use instead
Because @State is tied to the view instance, navigating away and back can recreate the view and reset the value to zero. That makes @State ideal for UI-only state such as whether a sheet is open, the current text in a search field, or a temporary counter. It is the wrong choice for data that must survive app restarts or be shared between sibling views.
| Scenario | Recommended tool | Why |
|---|---|---|
| Toggle inside one view | @State | View-local, transient, value type |
| Class instance shared across views | @StateObject | Persists across view recomputation; owned by the view that creates it |
| State observed from many places | ObservableObject + @EnvironmentObject | Explicit dependency injection through the environment |
| Data that survives app restarts | UserDefaults, file storage, or database | @State is memory only and resets on view recreation |
If you need a class instance that should not be recreated when other state changes, replace @State with @StateObject. For example:
class TimerModel: ObservableObject {
@Published var seconds = 0
}
struct TimerView: View {
@StateObject private var model = TimerModel()
var body: some View {
Text("\(model.seconds)s")
}
}
Here model survives view recomputation triggered by other @State properties in the same view, unlike a plain @State value.
Practical verification
- Add a @State counter and a Button to a SwiftUI view. Confirm the Text updates on every tap.
- Wrap a @State mutation in DispatchQueue.global().async without returning to the main queue. Observe whether the UI updates inconsistently.
- Replace a @State class property with @StateObject and change unrelated state. Confirm the class instance is not reinitialized.
Limitation
@State cannot store reference types that require stable identity, and it does not persist across view recreation. If your view is embedded in a NavigationStack and the user pops back, SwiftUI may rebuild the destination view and reset @State to its initial value. Treat @State as scratchpad memory for the current UI frame, not as a source of truth.
Closing checklist
- Declare @State only inside the View struct that owns the UI.
- Mutate @State on the main actor.
- Use @StateObject for class instances that must persist across view recomputation.
- Do not use @State for data that needs to outlive the view.
If your state outlives a single screen or needs to be read by multiple views, move it up to an observable object or the environment. Keep @State for the temporary, view-local details that make the interface feel responsive.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.