Fatal error: Duplicate keys of type 'SOMETYPE' were found in a Dictionary.

Hi! I have a strange error in my SwiftUI app and need a clue how to solve this issue. I work on a timetable like app where I hold the data in the App struct

@StateObject private var appData = VorlesungsHandler()

and the classes are defined as

public class VorlesungsHandler : ObservableObject {
    let dayList : [Weekdays] = [.Montag, .Dienstag]
    @Published var vorlesungen : [Vorlesung] = [ Vorlesung(…), … ]
    …
}

and

class Vorlesung : Hashable, Equatable, Identifiable, ObservableObject {
    @Published var subject: ModuleType
    @Published var person : Personen
    let vID = UUID()
    …
}

The appData object is passed down to the views and subviews via

.environmentObject(appData)

and it seems to work well.

Now I face an error when I change the array „vorlesungen“ in Appdata via

    func updateVorlesung(oldID : UUID?, newV : Vorlesung) {
        if oldID == nil {
            self.vorlesungen.append(newV)
            return
        }
        
        for i in 0..<self.vorlesungen.count {
            if self.vorlesungen[i].vID == oldID! {
                self.vorlesungen.remove(at: i)
                self.vorlesungen.append(newV)
                break
            }
        }
    }

The error message is

Fatal error: Duplicate keys of type 'Vorlesung' 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.

This error does not occur every time but 30 to 50% of the changes. In my tests I did also add

self.objectWillChange.send()

in front of all array-change commands but it does not help.

Any clue how to solve this issue?

Accepted Reply

I did post the same question on Swift Forum and got a solution:

Currently your hash and == are using completely different properties, which leads to conflict between them. Are two Vorlesung with the same subject but different vID equal to each other, or not? According to == they are equal, but according to hash they are not. We need to fix that

You need to decide what makes two Vorlesung the same, and use these properties in both == and hash. Usually in a struct it's all of the properties, and that's handled by the autogenerated implementation

so a change in my code did solve the issue:

static func == (lhs: Vorlesung, rhs: Vorlesung) -> Bool {
    let result = lhs.subject.compare(rhs.subject) == .orderedSame
    return result
}
func hash(into hasher: inout Hasher) {
    hasher.combine(subject)
}
  • Saw that there, nice that you found a solution that solves the root cause.

Add a Comment

Replies

vorlesungen in the for loop seems to be an array, not a dictionary.

But anyways, it seems that while modifying the array within the loop is the issue?

Maybe just use array find methods to search for the index of the id, without the loop, and then remove the object in that index.

That makes no difference. Using following code instead of the loop gives the same error:

        self.vorlesungen.removeAll { value in
            value.vID == oldID
        }

The dictionary could be the Hash.

Where do you call updateVorlesung ?

Could it be that subject or person already exists (that could explain the message: This usually means either that the type violates Hashable's requirements.

In anycase, you should add print statements in updateVorlesung to check what is the content of vorlesungen and what is the object you insert, to find if there is a duplicate.

> Where do you call updateVorlesung ?

I show a separate data-entry Window and I call it in a Button("Ok").action.

The Class Vorlesung is Hashable:

    func hash(into hasher: inout Hasher) {
        hasher.combine(vID) // UUID
    }

The debug output of the class properties and the Hash does not show a duplicate hash value. It is also very unlikely that there is a duplicate hash because of the used UUID…

Any other ideas how to solve this?

What if you do the updates to a temporary array and then replace the @Published array with the temporary?

Tested right now. No difference. The error message is

> 2023-03-16 08:06:52.903332+0100 ITR-Stundenplan[91211:778159] Fatal error: Duplicate keys of type 'Vorlesung' 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.

I wonder if there is perhaps a timing issue, where the data is accessed during the change although I use objectWillChange:

        self.objectWillChange.send()
        self.vorlesungen = newList

Really strange…

Have a look at this thread (Rincewind answer). May be you have a similar issue: https://developer.apple.com/forums/thread/726544

Now I was able to build a simplified example which shows the error. I have two days and a click on the button does send it to the other day and back. It does sometimes crash on the first call but on another run on the 3rd call. I post the code below. Perhaps it gives a clue what's wrong:

import SwiftUI
enum Weekdays : String, CaseIterable {
    case Montag
    case Dienstag
    case Mittwoch
    case Donnerstag
    case Freitag
    case Samstag
    //case Sonntag
}

class Vorlesung : Hashable, Equatable, Identifiable, ObservableObject {
    @Published var subject : String
    @Published var day     : Weekdays
    let vID = UUID()
    
    init(subject: String, day : Weekdays) {
        self.subject = subject
        self.day     = day
    }
    
    static func == (lhs: Vorlesung, rhs: Vorlesung) -> Bool {
        let result = lhs.subject.compare(rhs.subject) == .orderedSame
        return result
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(vID) // UUID
    }
}

public class VorlesungsHandler : ObservableObject {
    let dayList : [Weekdays] = [.Montag, .Dienstag]//, .Mittwoch, .Donnerstag, .Freitag]
    @Published var vorlesungen : [Vorlesung] = [
        Vorlesung(subject: "banana", day: .Montag),
        Vorlesung(subject: "strawberry", day: .Montag),
        Vorlesung(subject: "apple", day: .Montag),
        Vorlesung(subject: "pear", day: .Montag)
    ]
    func updateVorlesung(oldID : UUID?, newV : Vorlesung) {
        self.vorlesungen.removeAll { value in
            value.vID == oldID
        }
        self.vorlesungen.append(newV)
    }
}

struct PlanEntryView: View {
    @EnvironmentObject var appData : VorlesungsHandler
    @ObservedObject var setup : Vorlesung
    @State private var showDataView : Bool = false
    
    var body: some View {
        ZStack {
            Button(setup.subject) {
                let switchedDay : Weekdays = setup.day == .Montag ? .Dienstag : .Montag
                appData.updateVorlesung(oldID: setup.vID, newV: Vorlesung(subject: setup.subject, day: switchedDay))
            }
        }
    }
}


struct RoomView: View {
    @EnvironmentObject var appData : VorlesungsHandler
    var day  : Weekdays

    var body: some View {
        VStack() {
            VStack {
                ForEach(appData.vorlesungen, id: \.self) { value in
                    if (value.day == day) {
                        PlanEntryView(setup: value)
                            .environmentObject(appData)
                    }
                }
            }
        }
    }
}

struct DayView: View {
    @EnvironmentObject var appData : VorlesungsHandler

    var day : Weekdays
    var body: some View {
        VStack {
            Text(day.rawValue)
            HStack {
                RoomView(day: day)
                    .environmentObject(appData)
            }
            Spacer()
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var appData : VorlesungsHandler

    var body: some View {
        VStack {
            ForEach(appData.dayList, id: \.self) { value in
                DayView(day: value)
                    .environmentObject(appData)
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    @StateObject static var previewData = VorlesungsHandler()

    static var previews: some View {
        ContentView()
            .environmentObject(previewData)
    }
}

@main
struct crasherApp: App {
    @StateObject private var appData = VorlesungsHandler()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(appData)
        }
    }
}

With this change I got it working without the error.

	func updateVorlesung(oldID : UUID?, newV : Vorlesung) {
		if let oldID {
			if let vorlesung = vorlesungen.first(where: { $0.vID == oldID}) {
				vorlesung.day = newV.day
			}
		} else {
			self.vorlesungen.append(newV)
		}
		self.objectWillChange.send()
//		self.vorlesungen.removeAll { value in
//			value.vID == oldID
//		}
//		self.vorlesungen.append(newV)
	}

Though here you do not actually need to create a new Vorlesung if the object already exists in the container, just pass the new/changed weekday as the second parameter to the method. Check out what works for you.

Whether this fits in your overall design is another matter :)

Thanks for your help. I will test it tomorrow and report back.

Using above code I got still the same error although it needs more clicks until it occurred. I wonder if there is a general issue in SwiftUI or in my data model.

I did post the same question on Swift Forum and got a solution:

Currently your hash and == are using completely different properties, which leads to conflict between them. Are two Vorlesung with the same subject but different vID equal to each other, or not? According to == they are equal, but according to hash they are not. We need to fix that

You need to decide what makes two Vorlesung the same, and use these properties in both == and hash. Usually in a struct it's all of the properties, and that's handled by the autogenerated implementation

so a change in my code did solve the issue:

static func == (lhs: Vorlesung, rhs: Vorlesung) -> Bool {
    let result = lhs.subject.compare(rhs.subject) == .orderedSame
    return result
}
func hash(into hasher: inout Hasher) {
    hasher.combine(subject)
}
  • Saw that there, nice that you found a solution that solves the root cause.

Add a Comment