yout_ishida
@yout_ishida (悠斗 石田)

Are you sure you want to delete the question?

If your question is resolved, you may close it.

Leaving a resolved question undeleted may help others!

We hope you find it useful!

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.

0

1Answer

SwiftUI および SwiftData の「重複するキーのタイプ」エラーは、通常、データ モデルに競合するキーがある場合に発生します。これは、多くの場合、エンティティ構成または主キー設定が正しくないことが原因です。この問題のトラブルシューティングと解決方法は次のとおりです。
https://oke.io/YmMkiZq![16x9_A_digital_illustration_depicting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3915635/b5b28ddc-28c7-ca73-563c-9e0ff217c386.png)

解決手順
データ モデルの確認:
データ モデル内の各エンティティに一意の主キーがあることを確認します。これは、SwiftData がエンティティを正しく管理するために不可欠です。

エンティティ リレーションシップの確認:
エンティティ間にリレーションシップがある場合は、それらが正しく設定され、循環参照や競合がないことを確認します。

コードの検査:
重複するインスタンスまたはキーを作成しようとしている可能性のあるコードを探します。これは、同じオブジェクトを誤って複数回挿入した場合に発生する可能性があります。

古いデータを削除する:
データ モデルに変更を加える場合は、新しいモデルと競合する可能性のある古いデータをすべて削除してください。これを行うには、シミュレーターをリセットするか、デバイスからアプリを削除します。
一意の識別子を使用する:
新しいエンティティを作成するときは、各インスタンスに一意の識別子を使用します。これは、UUID または増分 ID を使用して実行できます。
移行を実行する:
Core Data モデルを変更した場合は、移行を適切に処理するようにしてください。これにより、データの競合なしで変更を管理できます。
デバッグ:
作成されるキーとエンティティをログに記録するデバッグ ステートメントを追加します。これにより、重複キーがどこから来ているのかを特定できる場合があります。

0Like

Your answer might help someone💌