SwiftUI View Identity: Why Your Views Re-Render (and How to Fix It)

SwiftUI decides whether to update an existing view or throw it away and create a fresh one based on a concept called identity. Misunderstanding it is behind a specific, recognizable bug: a view’s @State resetting unexpectedly, or a list row losing its animation state when the list changes.

Structural Identity: Position in the View Tree

By default, SwiftUI identifies a view by its type and its position in the view hierarchy. Two views of the same type in the same structural position are treated as “the same view” across updates, and SwiftUI preserves their @State between redraws.

if isEditing {
    EditForm()
} else {
    DisplayView()
}

Here, EditForm and DisplayView occupy the same structural position (the body of this if), but they’re different types — so toggling isEditing destroys one and creates the other from scratch. Any @State inside either one is lost every time you switch, which is usually what you want in this case.

Where Structural Identity Breaks Down: Lists

Structural identity alone isn’t enough for a dynamic list, because items can be inserted, removed, or reordered. Without more information, SwiftUI can’t tell whether the item now at index 2 is the same logical item that used to be at index 2, or a different one that moved there.

Explicit Identity: Identifiable and id()

This is what ForEach‘s Identifiable requirement (or an explicit id: parameter) is for — it gives SwiftUI a stable identity per item that survives reordering:

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

ForEach(tasks) { task in
    TaskRow(task: task)
}

Now if tasks is reordered, SwiftUI matches each TaskRow to its Task.id rather than its index, so any in-progress state or animation on that row follows the correct item.

Forcing a Reset with .id()

You can also use .id() deliberately to force SwiftUI to treat a view as brand new — useful when you want its state to reset, such as restarting a form when the underlying record changes:

EditProfileForm(user: selectedUser)
    .id(selectedUser.id) // resets all @State inside when selectedUser changes

The Debugging Checklist

If a view’s state is resetting when it shouldn’t: check whether its type or structural position is changing between updates (an if/else branching to different view types is a common cause). If a list row’s state is following the wrong item after a reorder: check that you’re using a stable, unique id rather than the array index.