You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftData首次启动触发「Duplicate keys of type」致命错误求助

SwiftData开发中的重复键致命错误排查

错误信息

Thread 1: Fatal error: Duplicate keys of type 'Bland' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or that members of such a dictionary were mutated after insertion.

错误触发特征

  • 仅在应用首次安装启动时出现,通常在启动后约1分钟触发
  • 代码和服务器数据无变更时,错误出现存在不确定性

核心代码片段

数据刷新逻辑

func refleshBlandsData() async throws {
    if let blandsOnServer = try await DataModel.shared.getBlands() {
        await MainActor.run {
            blandsOnServer.forEach { blandOnServer in
                if let blandOnLocal = blandList.first(where: { $0.code == blandOnServer.code }) {
                    blandOnLocal.update(serverBland: blandOnServer)
                } else {
                    modelContext.insert(blandOnServer.bland)
                }
            }
        }
    }
}

StockListView核心逻辑

struct StockListView: View {
    @Environment(.modelContext) private var modelContext
    @Query(sort: \Bland.sname) var blandList: [Bland]
    @Query var users: [User]
    @State private var isNotLoaded = true
    @State private var isLoading = false
    @State private var loadingErrorState = ""

    var body: some View {
        NavigationStack {
            List {
                ForEach(blandList, id: \.code) { bland in
                    NavigationLink(value: bland) {
                        Text(bland.sname)
                    }
                }
            }
            .navigationTitle("Stock List")
            .onAppear {
                doIfFirst()
            }
        }
    }

    // 首次启动加载数据
    func doIfFirst() {
        if isNotLoaded {
            loadDataWithAnimationIfNotLoading()
            isNotLoaded = false
        }
    }

    // 避免重复触发的加载逻辑
    func loadDataWithAnimationIfNotLoading() {
        if !isLoading {
            isLoading = true
            Task {
                do {
                    try await loadData()
                } catch {
                    loadingErrorState = "Data load failed: \(error.localizedDescription)"
                }
                isLoading = false
            }
        }
    }

    // 从服务器取数并插入SwiftData
    func loadData() async throws {
        if let blandsOnServer = try await DataModel.shared.getBlands() {
            for bland in blandsOnServer {
                if !blandList.contains(where: { $0.code == bland.code }) {
                    modelContext.insert(bland.bland)
                }
            }
        }
    }
}

实体定义

Bland实体

@Model
class Bland: Identifiable {
    @Attribute(.unique) var code: String
    var sname: String
    @Relationship(deleteRule: .cascade, inverse: \CrossZansu.bland)
    var zansuList: [CrossZansu]
    @Relationship(deleteRule: .cascade, inverse: \Trade.bland)
    var trades: [Trade]
}

CrossZansu实体

@Model
class CrossZansu: Equatable {
    @Attribute(.unique) var id: String
    var brocker: Broker?
    var zansu: Int
    var bland: Bland?
}

问题解答

1. 该问题是否与SwiftData中@Relationship、@Attribute(.unique)的管理逻辑有关?

有关系,核心两点:

  • @Attribute(.unique)的并发冲突:首次启动时异步加载流程可能存在竞态(比如onAppear重复触发、Task并发执行),导致同一code的Bland被多次插入。尽管用blandList.contains做了判断,但@Query返回的blandList是动态更新的,异步场景下判断时机可能滞后,出现误判。
  • @Relationship的Hash稳定性问题:SwiftData的@Model类默认实现Hashable,但关联属性(如zansuList)的变更可能导致实体Hash值突变。如果SwiftData内部用Dictionary管理实体,Hash值不稳定会直接引发重复键错误。

2. SwiftData实体实现Equatable(如CrossZansu)有哪些潜在陷阱?

  • 覆盖SwiftData默认管理逻辑:@Model类的Equatable、Hashable由SwiftData自动维护,手动实现会破坏内部唯一性识别逻辑,导致实体状态和业务判断不一致。
  • 循环引用风险:实现Equatable时如果比较关联对象(比如CrossZansu的bland),会触发循环比较(Bland的zansuList又包含CrossZansu),引发无限递归或性能问题。
  • 与@Attribute(.unique)规则冲突:手动实现的Equatable逻辑如果和@Attribute(.unique)定义的唯一性规则不符,会导致SwiftData内部状态和业务逻辑产生分歧,引发数据一致性问题。

3. 调试SwiftData「Duplicate keys」错误的推荐方法有哪些?

  • 跟踪Hash值变化:给Bland手动实现Hashable,在hash(into:)中打印日志,确认Hash值是否存在突变:
    extension Bland {
        func hash(into hasher: inout Hasher) {
            hasher.combine(code)
            print("Bland hash for code \(code): \(hasher.finalize())")
        }
    }
    
  • 排查并发插入:在modelContext.insert前后添加日志,记录插入的code值,确认是否存在重复插入。同时优化加载逻辑,用原子性标志或Task取消机制防止并发执行。
  • 启用SwiftData调试日志:在Xcode中添加启动参数-com.apple.CoreData.Logging.stderr 1,输出底层操作日志,跟踪实体插入、更新的完整流程。
  • 断点定位崩溃点:在错误对应的崩溃位置设置断点,查看调用栈,确认是SwiftData内部哪个Dictionary出现重复键,反推触发源。
  • 临时隔离关联属性:注释掉Bland的zansuList和trades属性,测试错误是否消失,快速定位是否是关联属性导致的Hash不稳定。

内容的提问来源于stack exchange,提问作者yuto_zutomayo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 22:34:57