最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

swift - Fatal Error "Duplicate keys of type" Occurs on First Launch - Stack Overflow

programmeradmin1浏览0评论

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: \.code) { 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 brocker: Broker?
    var zansu: Int
    var bland: Bland?
}

Trade:

@Model
class Trade {
    @Relationship(deleteRule: .nullify)
    var user: User?
    var date: Date
    var bland: Bland
    var amountOfKabus: Int
    var broker: Broker
    
    var k_saishu: String?
    var tradeState: TradeState
    
    init(user: User?, date: Date, bland: Bland, amountOfKabus: Int, broker: Broker) {
        self.user = user
        self.date = date
        self.bland = bland
        self.amountOfKabus = amountOfKabus
        self.broker = broker
        self.tradeState = .traiding
    }
    var id: String {
        (user?.id.description ?? "nil") + date.description + bland.code + amountOfKabus.description + broker.rawValue
    }
}

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.

All of the StockListView code is listed below.

import SwiftUI
import SwiftData
import Flow
import os

struct StockListView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.modelContext) private var modelContext
    @Query(sort: \Bland.sname) var blandList: [Bland]
    @Query var users: [User]
    @Query var crossZansuList: [CrossZansu]
    
    @AppStorage("last_load") var storedLastLoadTime = Date().timeIntervalSinceReferenceDate
    var lastLoadTime: Date {
        set {storedLastLoadTime = newValue.timeIntervalSinceReferenceDate}
        get {return Date(timeIntervalSinceReferenceDate: storedLastLoadTime)}
    }

    @State var filterParameter = FilterParameter()
    var displayBlands: [Bland] {
        return filterParameter.filterBlands(allBlands: blandList)
    }
    
    @State var isShowSettingView = false
    @State var isShowFilteringView = false
    @State var sortState = SortState.rateOfReturn
    
    // Related to loading
    @State var isNotLoaded = true // Only true on the first app launch to prevent loading data after onAppear is called for the second time
    @State private var isLoading = false
    @State var loadingErrorState = ""

    // MARK: - View Components
    var body: some View {
        rootView
            .sheet(isPresented: $isShowSettingView, content: {
                SettingView()
            })
            .sheet(isPresented: $isShowFilteringView, content: {
                BlandFilterView(filterParameter: $filterParameter, displayBlands: displayBlands)
            })
            .accentColor(Color.green)
            .onAppear {
                doIfFirst()
            }
    }
    private var rootView: some View {
        NavigationStack {
            navigationContent
                .navigationTitle("Stock Search")
                .navigationDestination(for: Bland.self) { bland in
                    AStockDetailView(bland: bland)
                }
                .toolbar {
                    ToolbarItem(placement: .topBarLeading) {
                        Button( action: {
                            isShowSettingView.toggle()
                        }) {
                            Image(systemName: "gear")
                        }
                    }
                    ToolbarItemGroup(placement: .topBarTrailing) {
                        Menu {
                            Picker("", selection: $sortState) {
                                ForEach(SortState.allCases, id: \.title) { sortState in
                                    Text(sortState.title)
                                }
                                
                            }
                        } label: {
                            Image(systemName: "arrow.up.arrow.down")
                        }
                        Button {
                            isShowFilteringView.toggle()
                        } label: {
                            Image(systemName: "line.3.horizontal.decrease.circle")
                        }
                        
                    }
                }
        }
    }
    private var navigationContent: some View {
        scrollView
            .appBackground()
            .refreshable {
                await loadDataWithAnimationIfNotLoading()
            }
    }
    private var scrollView: some View {
        ScrollView {
            HStack(spacing: 2) {
                Text("Last Updated:")
                if isLoading {
                    Text("Fetching data...")
                        .font(.body)
                        .fontWeight(.regular)
                        .padding(.leading)
                    ProgressView()
                        .progressViewStyle(CircularProgressViewStyle())
                } else {
                    if loadingErrorState.isEmpty {
                        Text("\(lastLoadTime.convertedString(format: "yyyy/MM/dd HH:mm"))")
                    } else {
                        Text(loadingErrorState)
                            .foregroundStyle(Color.red)
                    }
                }
                Spacer()
            }
            .foregroundStyle(.secondary)
            .fontWeight(.semibold)
            .padding(.horizontal, 14)
            if blandList.isEmpty {
                emptyStateOfEmptyBlands
            } else if displayBlands.isEmpty {
                emptyStateOfFiltering
            }

            let sortedBlandList = sortBlandList(blandList, by: sortState)
            ForEach(sortedBlandList, id: \.code) { bland in
                if displayBlands.contains(bland) {
                    NavigationLink(value: bland) {
                        tileView(bland)
                            .contextMenu(menuItems: {
                                Button(action: {
                                    UIPasteboard.general.string = bland.code
                                }){
                                    Label("Copy Stock Code", systemImage: "doc.on.doc")
                                }
                            })
                            .padding(.horizontal, 14)
                            .padding(.vertical, 6)
                    }
                }
            }
        }
    }
    private func tileView(_ bland: Bland) -> some View {
        VStack {
            HStack(alignment: .lastTextBaseline) {
                Text(bland.yutai.prefix(20))
                    .lineLimit(1)
                    .minimumScaleFactor(0.8)
                    .font(.title2)
                    .fontWeight(.semibold)
                Spacer()
                FavoriteStar(isFavorite: Binding(get: {
                    bland.isFavorite
                }, set: { bool in
                    bland.isFavorite = bool
                }))
            }
            Text("\(bland.benefitType.rawValue)・\(bland.realWorth) yen equivalent")
                .putTo(side: .left)
                .foregroundStyle(.secondary)
            if let note = bland.note {
                HStack(spacing: 2) {
                    Image(systemName: "note.text")
                        .foregroundStyle(Color.accentColor)
                    
                    Text(note)
                        .lineLimit(2)
                        .multilineTextAlignment(.leading)
                    Spacer(minLength: 0)
                }
                .padding(.top, 5)
                .padding(.bottom, -5)
                .padding(.leading, -2)
            }
            let columnCount: Int = bland.zansuList.isEmpty ? 3 : 4
            let columns: [GridItem] = Array(repeating: .init(.flexible(), alignment: .leading), count: columnCount)
            LazyVGrid(columns: columns, spacing: 2) {
                label(bland.code, systemName: "tag")
                label(bland.k_saishu_formatted(), systemName: "calendar")
                if let maxRateOfReturn = bland.maxRateOfReturn {
                    label(maxRateOfReturn.description + "%", systemName: "chart.bar")
                        .foregroundStyle(maxRateOfReturn.labelColorIfMinusIsRed())
                }
                label("\(bland.p_saitei) million yen", systemName: "banknote.fill")
            }
            .lineLimit(1)
            .minimumScaleFactor(0.8)
            let isUserSet = users.isUserSet
            let bottomColumns: [GridItem] = [GridItem(.flexible(maximum: 50), spacing: 0)] + Array(repeating: GridItem(.flexible(), spacing: 0), count: isUserSet ? 3 : 2)
            let brokersOfHasInvestryOrNowHolding: [Broker] = brokersOfHasInvestryOrNowHolding(bland: bland)
            if !brokersOfHasInvestryOrNowHolding.isEmpty {
                LazyVGrid(columns: bottomColumns, spacing: 3) {
                    GridRowTitle("")
                    GridRowTitle("Inventory")
                    GridRowTitle("Currently Held")
                    if isUserSet {
                        GridRowTitle("Available for Trading")
                    }
                    ForEach(brokersOfHasInvestryOrNowHolding, id: \.self) { broker in
                        ForEach(0..<(isUserSet ? 4 : 3)) { _ in
                            Divider()
                        }
                        Text(broker.shortName)
                            .font(.headline)
                        if let inventryState = bland.zansuList.first(where: {$0.brocker == broker})?.inventryState {
                            HStack(spacing: 3) {
                                Rectangle()
                                    .frame(width: 3, height: 20)
                                    .cornerRadius(5)
                                    .foregroundStyle(inventryState.color)
                                Text(inventryState.rawValue)
                            }
                        } else {
                            Text("None")
                                .foregroundStyle(.secondary)
                        }
                        if isUserSet {
                            flowView(users: purchasedUsers(bland: bland, broker: broker))
                            flowView(users: purchasableUsers(bland: bland, broker: broker))
                        } else {
                            let tradesFilteredByBroker = bland.trades.filter({$0.broker == broker})
                            VStack {
                                ForEach(tradesFilteredByBroker, id: \.id) { trade in
                                    Text(trade.amountOfKabus.description + " shares")
                                        .greenBackground()
                                }
                            }
                        }
                    }
                }
                .padding(.top)
                .lineLimit(1)
                .minimumScaleFactor(0.8)
            }
        }
        .tile()
        .foregroundStyle(Color(.label))
        .dyanamicShadow(colorScheme: colorScheme)
    }
    private func label(_ text: String, systemName: String) -> some View {
        HStack(spacing: 5) {
            Image(systemName: systemName)
                .resizable()
                .scaledToFit()
                .frame(width: 16)
                .foregroundStyle(.primary)
                .foregroundStyle(.green)
            Text(text)
        }
    }
    private func flowView(users: [User]) -> some View {
        return HFlow(horizontalAlignment: .leading, verticalAlignment: .center, horizontalSpacing: 5, verticalSpacing: 1, justified: false, distributeItemsEvenly: false) {
            ForEach(users, id: \.id) { user in
                Text(user.name.prefix(1))
                    .foregroundStyle(Color.accentColor)
                    .font(.body)
                    .bold()
                    .padding(.horizontal, 5)
                    .background(Color.accentColor.opacity(0.1))
                    .cornerRadius(7)
            }
        }
    }

发布评论

评论列表(0)

  1. 暂无评论