Rows keep a @State value. After removing the first of three items, which state does each remaining row show when the list is driven by indices, and by ids?
Predict first
After removing “A”, which origin state do rows B and C hold in each version?
import UIKit
import SwiftUI
nonisolated(unsafe) var shown: [String: String] = [:]
@MainActor final class Hook { static var remove: (() -> Void)? }
struct Row: View {
let title: String
@State private var origin: String? = nil
var body: some View {
shown[title] = origin ?? "-"
return Text(title).onAppear { if origin == nil { origin = title } }
}
}
struct ByIndex: View {
@State var items = ["A", "B", "C"]
var body: some View {
let _ = (Hook.remove = { items.removeFirst() })
return VStack { ForEach(items.indices, id: \.self) { Row(title: items[$0]) } }
}
}
struct ByID: View {
@State var items = ["A", "B", "C"]
var body: some View {
let _ = (Hook.remove = { items.removeFirst() })
return VStack { ForEach(items, id: \.self) { Row(title: $0) } }
}
}
@MainActor func trial(_ name: String, _ root: some View) {
shown = [:]
let host = UIHostingController(rootView: root)
let window = UIWindow(frame: CGRect(x: 0, y: 0, width: 300, height: 400))
window.rootViewController = host
window.makeKeyAndVisible()
host.view.layoutIfNeeded()
RunLoop.main.run(until: Date().addingTimeInterval(0.3))
Hook.remove?()
host.view.layoutIfNeeded()
RunLoop.main.run(until: Date().addingTimeInterval(0.3))
print(name, "B and C carry the state created for", shown["B"]!, "and", shown["C"]!)
}
MainActor.assumeIsolated {
trial("indices:", ByIndex())
trial("ids: ", ByID())
}
- indices: A and B / ids: B and C
- indices: B and C / ids: B and C
- indices: A and B / ids: A and B
Mental model. Seat numbers on a train. If the ticket says “seat 2” and a passenger leaves, the next person moves into seat 2 and inherits the bag left on it. Tickets with the passenger’s name would keep each bag with its owner.
SwiftUI stores @State against a view’s identity, and in a ForEach the identity is the value of the id key path (Q260). With ForEach(items.indices, id: \.self) the identities are 0, 1, 2. Remove the first item and they become 0, 1: the view with identity 0, which was created for “A” and stored “A” in its state, is now simply updated with the title “B”; identity 1 is updated from “B” to “C”. The row for B therefore carries the state created for A, and the row for C carries B’s: the output is “A and B”. With ForEach(items, id: \.self) the identities are the values themselves, so removing “A” just removes that row and “B” and “C” keep their own state (“B and C”).
So the rule is to give a row an id that belongs to the thing it shows (a stable model id), never its position, as soon as rows have state, animations, focus, or a running task. This is also why .id(UUID()) on a view resets its state on every update, and why the identity must be unique and stable: an unstable id throws state away, and a duplicate one gives undefined behaviour. The wrong-state bug is easy to miss because the text looks right: the title comes from the new data, while the state (a selection, a text field’s draft, a toggle) silently belongs to the previous occupant of that identity.
Index identities are positions, so the surviving views are reused with shifted data; id identities travel with the item.
Common traps
- Using
indicesor the array position as the id. State and animations attach to the slot, not to the item. - Using a non-unique value (a name, a category) as the id. Duplicates give unpredictable updates; use a stable identifier on the model.
They might follow up with
- Why does
List(items)needIdentifiable? So it can use the model’s stableidinstead of a position (Q270). - How do you deliberately reset state? Change the view’s identity:
.id(itemID)on the container, or switch between branches of anif(Q260).
Related questions
- 27.4.4 What is a reliable routine for answering “what does this print?” under interview pressure?
- 27.1.2 What do closures print when they capture a loop variable, a shared variable, or a copy in a capture list?
- 8.1.2 What is structural versus explicit identity in SwiftUI, and what happens to state when identity changes?
- 8.3.1 What do @State, @Binding and @Bindable do, and who owns the data?
- 8.6.1 How do List, ForEach and the lazy stacks differ, and what do they need from your data?
← In what order do these prints appear when a main-actor function… What is a reliable routine for answering “what does this print?”… →