swiftui · essay · identity · architecture

SwiftUI Identity: Stable IDs, Stable Behavior

Published March 30, 2026 · 3 min read · intermediate

The confusion

A list renders correctly, but after a small update, row animations glitch, toggles reset, or expanded states jump to the wrong item. Developers often blame SwiftUI diffing as “unpredictable”.

In most cases, SwiftUI is not being random. Identity is.


Why this is confusing

SwiftUI makes rendering feel declarative: you describe UI from data and let the framework reconcile changes. That works beautifully until identity assumptions are wrong.

If IDs change between renders, SwiftUI may treat old rows as removed and new rows as inserted. From the framework perspective, that is correct. From your product perspective, it feels like state loss and animation bugs.

This mismatch is subtle because everything still compiles and often looks fine in static previews. Problems appear under real mutations: sorting, filtering, optimistic updates, and pagination.


The mental model

SwiftUI does not preserve view instances. It preserves identity across state transitions.

Identity answers one question: “Is this the same logical item as before?”

When identity is stable, SwiftUI can:

  • keep row-local state where expected
  • animate updates as changes, not replacements
  • reduce visual discontinuity

When identity is unstable, reconciliation becomes replacement-heavy and UX quality drops.


A small proof

Unstable identity example:

ForEach(items.indices, id: \.self) { index in
    ItemRow(item: items[index])
}

If the array order changes, index-based IDs no longer map to logical item identity. A row that used to represent item A might now represent item B.

Stable identity example:

struct Item: Identifiable {
    let id: UUID
    var title: String
}

ForEach(items) { item in
    ItemRow(item: item)
}

Now identity follows the model lifecycle, not temporary position. That allows SwiftUI to reconcile updates correctly.

A second practical case is filtering: if filtered arrays recreate IDs on every fetch, identity changes even when content is same. Keep IDs generated at data creation boundary, not render boundary.


Why this matters in real apps

Stable identity has direct product impact. Interactive lists feel calmer because toggles, text field edits, and expanded sections stay attached to the right logical item. Animations also improve because inserts and updates are differentiated correctly.

It also improves architecture decisions. Teams become intentional about where IDs are created and how long they live. Data layers own identity. Views consume it.

In code reviews, identity quality is a useful smell test. When you see \.self on mutable value collections or index-based IDs on reorderable content, ask whether logical identity truly survives mutations.


Where this model breaks down

Not every collection needs custom stable IDs. For static content that never reorders or mutates, simpler IDs can be fine.

The model also gets tricky with composite models from multiple sources. If backend IDs are missing, teams may need deterministic synthetic IDs. Those should still be generated from stable domain fields, not transient UI state.

And even with perfect IDs, state can still reset if ownership is misplaced. Identity and ownership work together. One does not replace the other.


One sentence to remember

Stable IDs let SwiftUI understand continuity, and continuity is what users experience as correctness.

Next steps

If you want to connect identity with state ownership and recomposition behavior, these are the strongest follow-ups: