List rows reset their state after a reorder because id is the array index
Each row keeps its own per-row @State (an expanded/collapsed toggle). After the user reorders items, the wrong rows appear expanded. The rows are identified by their array index.
Requirements:
- Explain why the state attaches to the wrong row after a move.
- Fix identity so each row's state follows its data element, not its slot.
struct RowsView: View {
@State private var items: [String] = ["A", "B", "C"]
var body: some View {
List {
ForEach(Array(items.enumerated()), id: \.offset) { _, item in
RowView(title: item) // RowView has its own @State isExpanded
}
.onMove { from, to in items.move(fromOffsets: from, toOffset: to) }
}
}
}
Find and fix the bug.
Keying rows by array index ties identity to position, so after a reorder SwiftUI treats slot 0 as the same view and keeps its old state on new data. Give each element a stable Identifiable id so identity follows the data, not the slot.
- ✗Using the array index (an offset) as the row's identity
- ✗Believing SwiftUI tracks rows by contents rather than by the supplied
id - ✗Adding
.id(UUID())to force a rebuild instead of giving stable identity
- →What is the difference between structural identity and explicit
.id()identity? - →Why does a stable
Identifiableid preserve per-row@Stateacross a move?
The rows are identified by id: \.offset — the array index. Identity is therefore the position, so after a move SwiftUI still sees "row 0, row 1, row 2" and keeps each slot's existing view (and its @State) while feeding it the reordered data. Structural identity (the index) no longer matches the data.
Fix it by giving each element a stable identity that travels with the data:
struct Item: Identifiable {
let id = UUID()
var title: String
}
struct RowsView: View {
@State private var items: [Item] = [Item(title: "A"), Item(title: "B"), Item(title: "C")]
var body: some View {
List {
ForEach(items) { item in // uses Item.id — stable per element
RowView(title: item.title)
}
.onMove { from, to in items.move(fromOffsets: from, toOffset: to) }
}
}
}
Now identity is Item.id, which moves with the element, so each row's @State follows its data through the reorder. This is the difference between structural identity (position in the tree) and explicit identity (a stable value you supply).