SwiftUI & SwiftData: Fatal Error "Duplicate keys of type" Occurs on First Launch

I'm developing a SwiftUI app using SwiftData and encountering a persistent issue:

Error Message:

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.

Details:

Occurrence: The error always occurs on the first launch of the app after installation. Specifically, it happens approximately 1 minute after the app starts. Inconsistent Behavior: Despite no changes to the code or server data, the error occurs inconsistently. Data Fetching Process:

I fetch data for entities (Bland, CrossZansu, and Trade) from the server using the following process:

Fetch Bland and CrossZansu entities via URLSession. Insert or update these entities into the SwiftData context. The fetched data is managed as follows:


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)
                }
            }
        }
    }
}

This is a simplified version of my StockListView. The blandList is a @Query property and dynamically retrieves data from SwiftData:

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: \.self) { bland in
                    NavigationLink(value: bland) {
                        Text(bland.sname)
                    }
                }
            }
            .navigationTitle("Stock List")
            .onAppear {
                doIfFirst()
            }
        }
    }

    // This function handles data loading when the app launches for the first time
    func doIfFirst() {
        if isNotLoaded {
            loadDataWithAnimationIfNotLoading()
            isNotLoaded = false
        }
    }

    // This function ensures data is loaded with an animation and avoids multiple triggers
    func loadDataWithAnimationIfNotLoading() {
        if !isLoading {
            isLoading = true
            Task {
                do {
                    try await loadData()
                } catch {
                    // Capture and store any errors during data loading
                    loadingErrorState = "Data load failed: \(error.localizedDescription)"
                }
                isLoading = false
            }
        }
    }

    // Fetch data from the server and insert it into the SwiftData model context
    func loadData() async throws {
        if let blandsOnServer = try await DataModel.shared.getBlands() {
            for bland in blandsOnServer {
                // Avoid inserting duplicate keys by checking for existing items in blandList
                if !blandList.contains(where: { $0.code == bland.code }) {
                    modelContext.insert(bland.bland)
                }
            }
        }
    }
}

Entity Definitions:

Here are the main entities involved:

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 bland: Bland?
}

Trade:

@Model
class Trade {
    @Relationship(deleteRule: .nullify)
    var user: User?
    var bland: Bland
}

User:

class User {
    var id: UUID
    @Relationship(deleteRule: .cascade, inverse: \Trade.user)
    var trades: [Trade]
}

Observations:

Error Context: The error occurs after the data is fetched and inserted into SwiftData. This suggests an issue with Hashable requirements or duplicate keys being inserted unintentionally. Concurrency Concerns: The fetch and update operations are performed in asynchronous tasks. Could this cause race conditions? Questions:

Could this issue be related to how @Relationship and @Attribute(.unique) are managed in SwiftData? What are potential pitfalls with Equatable implementations (e.g., in CrossZansu) when used in SwiftData entities? Are there any recommended approaches for debugging "Duplicate keys" errors in SwiftData? Additional Info: Error Timing: The error occurs only during the app's first launch and consistently within the first minute.

The first thing I'd try is to avoid using \.self in the ForEach shown in the following code. A SwiftData object, or bland in this case, is Identifiable, and so you can use ForEach(blandList) directly, rather than using \.self to create the object hash.

struct StockListView: View {
    ...
    var body: some View {
        ...
                // ForEach(blandList, id: \.self) { bland in
                ForEach(blandList) { bland in
    ...
}

If that doesn't help, please provide a minimal project that contains only the code relevant to the issue, with detailed steps, for me to reproduce the issue. I'd take a closer look.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

SwiftUI & SwiftData: Fatal Error "Duplicate keys of type" Occurs on First Launch
 
 
Q