SwiftQARandom

What is Identifiable, and why does it matter for List/ForEach?

The protocol

Identifiable is a small protocol in the Swift standard library, not SwiftUI:

public protocol Identifiable<ID> {
    associatedtype ID: Hashable
    var id: ID { get }
}

Just an associated ID: Hashable and a property that returns it — a value that can answer which thing am I, over time.

Swift structs have no built-in answer to that. Two struct Item { var name: String } instances with the same name are indistinguishable — there’s no pointer to compare. Equatable asks do these values currently look the same; Identifiable asks are these the same logical entity, even if the contents differ. A todo item edited from “Buy milk” to “Buy oat milk” is != to its former self, but must stay == in identity, or the UI treats the edit as delete-and-insert.

Why List/ForEach care

SwiftUI diffs a new view tree against the old one rather than mutating views in place. For static views like Text or VStack, that diff is purely structural — same type at the same position means “same view, just update it.”

ForEach/List break that model because their children are data-driven: array count and order can change at runtime. Going from [A, B, C] to [B, C, D] looks, positionally, like “every row’s content changed” rather than “A was removed, D was appended.” That’s the wrong read, and it breaks real things:

  • Per-row @State (cursor position, @FocusState, an expanded toggle) slides onto the wrong item instead of following the item it belongs to.
  • Insertion/removal transitions cross-fade content instead of animating the actual add/remove.
  • List(selection:) loses track of which row is selected once the data reorders.

So ForEach/List require explicit identity: a stable id per element. SwiftUI diffs old ids against new ids to compute a minimal edit script, and only ids that survive keep their view’s state. Identifiable is just the standard way to supply that id — it’s why ForEach(items) { ... } works without a separate id: key path.

Three ways people get it wrong

  • id: \.self on a merely Hashable element. Identity becomes tied to value, so two equal-looking rows (both titled “New Item”) collapse to the same identity — editing one can visually jump to the other.
  • id: \.offset from .enumerated(). Identity becomes position, not content. Delete the first row and a focused TextField keeps its focus but now edits the wrong item, since “row 0” didn’t move with the data.
  • A computed, non-stable id, e.g. var id: String { UUID().uuidString }. A fresh id on every read means every diff looks like “all new rows” — no animations, no preserved state, visible flicker.

The fix is a stored, durable id assigned once and never changed — a database primary key, or a UUID() generated in init and stored as let:

struct Item: Identifiable {
    let id = UUID()   // fixed even as `name` mutates
    var name: String
}

Beyond ForEach/List

The same mechanism drives .sheet(item:), .popover(item:), .fullScreenCover(item:), and Table — each uses id to decide whether a new value should update the existing presentation or replace it. Identifiable also gained a primary associated type in Swift 5.7, so you can constrain generic code with any Identifiable<String>.