DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →

SwiftUI ForEach 提示重复 ID:为什么列表会错用其他行的状态

ForEach 出现重复 ID、行状态错位时,检查唯一且稳定的元素身份,区分记录 ID、显示值、数组下标及每次重建的 UUID。

ForEach 显示字母、标签或重复商品时,控制台出现 the ID ... occurs multiple times within the collection,有时还伴随行内容不更新、动画异常或输入状态落到另一行。数组允许重复值;问题出在把重复值同时用作视图身份。

本文讨论 iOS 13 及以后 SwiftUI 的 ForEach 元素标识。示例只使用基础 Identifiable 和 @State,不依赖新的列表编辑 API。

先看 id 的值,而不是数组能不能重复

下面的示例中,两个 A 的内容相同,身份也相同:

let letters = ["B", "A", "L", "A", "N", "C", "E"]

ForEach(letters, id: \.self) { letter in
    Text(letter)
}

id: \.self 表示元素值就是 ID。SwiftUI 需要用它区分同一 ForEach 中的各个元素,并在更新前后对应同一个元素。两个 A 无法满足这个要求;即使当前画面碰巧正确,也不应依赖重复 ID 下的表现。

对业务数组,可以在调试阶段检查真正传给 ForEach 的那一批 ID:

let ids = rows.map(\.id)
assert(Set(ids).count == ids.count, "rows 存在重复 ID")

rows 是待显示的模型数组。这只能检查当前批次的唯一性,还要继续确认同一个业务元素在刷新、排序之后是否保留原 ID。

把身份放进模型,并在创建元素时生成

下面的独立示例为每个字母位置建立自己的模型。相同字母可以出现多次,因为每个元素都有独立 ID:

import SwiftUI

struct LetterItem: Identifiable {
    let id = UUID()
    let text: String
}

struct LetterList: View {
    @State private var letters = "BALANCE".map {
        LetterItem(text: String($0))
    }

    var body: some View {
        VStack {
            ForEach(letters) { letter in
                Text(letter.text)
            }
        }
    }
}

此处模型数组保存在 @State 中,既有元素在视图更新时保留 ID。不要把生成数组的代码改成每次访问都会重新运行的计算属性;否则每次都会得到整套新 UUID,SwiftUI 会把所有行视为替换后的元素。

业务数据若已有稳定的记录主键,优先使用该主键。重新请求服务器时仍为同一条记录保留同一个 ID。只有确实新增一个元素时才生成本地 UUID,并让它随元素一起保存;若数据需要跨启动保留身份,也要把 ID 一起持久化。

用数组下标能消除警告,但未必修好问题

ForEach(Array(rows.enumerated()), id: \.offset) 能让当前数组的各个位置不同。对于内容和顺序固定的格子,这可以表达“位置就是身份”。但可删除、插入、筛选、排序的业务列表,位置通常不是记录本身。

例如原来的第 0 行处于展开状态,在最前面插入一条记录后,第 0 个位置变成新记录。若按下标保存身份,局部展开状态可能仍对应第 0 个位置,看起来就像新记录继承了旧记录的状态。仅仅没有重复 ID 警告,不代表身份选择正确。

同样,不要用会变化的标题、昵称、价格作为记录 ID。值一变,SwiftUI 就可能把原行当成另一行,局部状态和动画也会失去连续性。需要修改的是显示属性,记录身份应保持稳定。

如果只是展示一次性、固定顺序的字符槽,可以按槽位建立模型;如果字符槽本身会移动或插入,就需要能随槽位移动的身份。先确定界面想保留的是“第几个位置”的状态,还是“哪条记录”的状态,再选 ID。

检查完成后,再观察插入和排序

验收时准备两条显示内容完全相同、ID 不同的记录,让其中一行处于编辑或展开状态,再执行插入、删除和排序。状态应继续跟随那条记录,控制台也不再报告重复 ID。

若 ID 已唯一且稳定,而行内输入仍跳动,继续检查是否在更新时替换了整个模型、是否另有 .id(...) 强制重建,以及绑定是否仍指向正确记录。不要继续用随机 ID 掩盖问题;随机化会让 SwiftUI 每次都建立新身份,正好破坏需要保留的状态。