One of the most confusing things about SwiftUI is this:
“Why did my view recreate?”
“Why did my state reset?”
“Why did my animation restart?”
“Why did my ViewModel reload?”
The answer is almost always the same:
View identity changed.
SwiftUI is not UIKit.
Views are value types, not objects.
Understanding identity vs lifecycle is the key to mastering SwiftUI.
This post explains:
- why SwiftUI recreates views
- what identity actually means
- when state survives vs resets
- how
@State,@StateObject, andid()really work - how navigation and lists affect lifecycle
- how to stop accidental resets
🧠 The Core Mental Model
SwiftUI works like this:
- Views are descriptions, not instances
- SwiftUI compares view trees every update
- Identity determines whether state is reused or discarded
If SwiftUI thinks a view is the same identity → state is preserved
If SwiftUI thinks a view is new → state is recreated
Everything else follows from this.
🧱 Views Are Values, Not Objects
This surprises many developers:
struct MyView: View {
var body: some View { ... }
}
This struct can be created:
- dozens of times per second
- for every state change
- during animations
- during layout passes
That is normal.
Recreating the view struct ≠ recreating its state.
State is tied to identity, not struct instances.
🆔 What Is View Identity?
SwiftUI determines identity using:
- the view’s position in the hierarchy
- the view’s type
- any explicit id()
If any of these change → identity changes → state resets.
⚠️ The #1 Cause of Accidental State Reset
This pattern:
DetailView(item: item)
inside:
ForEach(items) { item in
DetailView(item: item)
}
If item.id changes or is unstable:
- SwiftUI thinks the view is new
- @State resets
- animations restart
- ViewModels reload
Rule:
📌 Identity must be stable and predictable.
🟦 @State — Lives as Long as Identity Lives
struct CounterView: View {
@State private var count = 0
}
@State:
- survives body recomputation
- resets when identity changes
- resets when the view leaves the hierarchy
If you see @State resetting:
👉 identity changed or view was removed.
🟩 @StateObject — Tied to View Identity
@StateObject private var vm = ViewModel()
@StateObject:
- is created once per identity
- survives view updates
- resets when identity changes
- survives parent state changes
Classic bug:
MyView(viewModel: ViewModel()) // ❌ recreated every render
Correct patterns:
- create with @StateObject
- or inject a stable instance from a parent
🔄 @ObservedObject — No Identity Protection
@ObservedObject var vm: ViewModel
This:
- does not preserve identity
- expects the parent to manage lifecycle
Use only when:
- the parent owns the object
- recreation is acceptable
⚠️ The id() Modifier: Nuclear Option
Text("Hello")
.id(UUID())
This forces SwiftUI to:
- treat the view as brand new
- discard all state
- restart animations
- recreate ViewModels
Use id() only when you intentionally want a reset:
- reset scroll position
- restart an animation
- force fresh state
Never use it casually.
🧭 Navigation & Lifecycle
Navigation adds and removes views.
When a view is:
- pushed → identity created
- popped → identity destroyed
- pushed again → brand new identity
That means:
- @State resets
- @StateObject resets
- tasks restart
If you need persistence across navigation:
- lift state up
- store in AppState
- store in a parent ViewModel
📦 Lists, ForEach & Identity
This is critical:
ForEach(items, id: \.id) { item in
RowView(item: item)
}
Good identity:
- stable id
- consistent ordering
Bad identity:
- UUID() generated on the fly
- index-based identity
- mutable IDs
Bad identity causes:
- row reuse bugs
- animation glitches
- state jumping rows
🔁 Why Animations Restart “Randomly”
Animations restart when:
- identity changes
- the view disappears and reappears
- id() changes
- parent view identity changes
If an animation restarts unexpectedly:
👉 identity changed.
🧠 Lifecycle vs Appearance
Important distinction:
- onAppear ≠ first creation
- onDisappear ≠ deallocation
Views may:
- appear multiple times
- disappear temporarily
- stay alive off-screen
Never put one-time setup in onAppear unless guarded.
Better:
.task { await load() }
or explicit lifecycle control in ViewModels.
🧪 Debugging Identity Problems
Ask these questions:
- Did this view move in the hierarchy?
- Did its id change?
- Did its parent recreate?
- Is this inside a conditional?
- Is this inside a list or navigation?
- Am I constructing objects inline?
Answering these almost always reveals the bug.
🧠 Identity Rules Cheat Sheet
✔ Stable ID → stable state
✔ Lift state up when needed
✔ Use @StateObject for ViewModels
✔ Avoid inline object creation
✔ Be deliberate with id()
✔ Understand when views leave the tree
🚀 Final Thoughts
SwiftUI doesn’t randomly reset state.
It does exactly what you ask — based on identity.
Once you understand:
- identity vs value
- state lifetime
- view hierarchy rules
SwiftUI becomes predictable, debuggable, and powerful.
Top comments (0)