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?

27.4.3 · L3 · Interview

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 indices or 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) need Identifiable? So it can use the model’s stable id instead 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 an if (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?”… →