DEV Community

Sebastien Lato
Sebastien Lato

Posted on

SwiftUI View Identity & Lifecycle: Why Views Recreate & State Resets

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, and id() 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 { ... }
}
Enter fullscreen mode Exit fullscreen mode

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

inside:

ForEach(items) { item in
    DetailView(item: item)
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

Correct patterns:

  • create with @StateObject
  • or inject a stable instance from a parent

🔄 @ObservedObject — No Identity Protection

@ObservedObject var vm: ViewModel
Enter fullscreen mode Exit fullscreen mode

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

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

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

or explicit lifecycle control in ViewModels.


🧪 Debugging Identity Problems

Ask these questions:

  1. Did this view move in the hierarchy?
  2. Did its id change?
  3. Did its parent recreate?
  4. Is this inside a conditional?
  5. Is this inside a list or navigation?
  6. 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)