Learn Courses My Dashboard

Generic struct 'ForEach' requires that 'Data' conform to 'Hashable'

Hi
It seems I still have to learn a lot from CWC even though I am into Module 5. This time XCode complains about:

Generic struct 'ForEach' requires that 'Data' conform to 'Hashable'

This happens in a Picker I am trying to create:

struct DataPicker: View {
    let label: String
    let data: [Data]
    
    @Binding var currentSelection: String
    
    var body: some View {
        HStack(alignment: .bottom) {
            Text("\(label):")
            
            Spacer()

            Picker("", selection: $currentSelection) {

// The following code works and does not give the error                
//                ForEach(0..<data.count, id: \.self) { idx in
//                    Text(data[idx].name).tag(data[idx].name)
//                }
                
                ForEach(data, id: \.self) { item in
                    Text(item.name).tag(item.name)
                }
            }
            .foregroundColor(.black)
        }
    }
}

The commented-out picker code works and does not generate the error. The Data class, which reads in from json, is as follows:

class Data: Identifiable, Decodable {
    var id:UUID?
    var name:String
    var mass:Double
    var fuelConsumption:Double
    var fuelTankCapacity:Double
    var lowFuelThreshold:Double
    
    init() {
        id = UUID()
        name = "My Unnamed Item"
        mass = 30
        fuelConsumption = 6
        fuelTankCapacity = 10
        lowFuelThreshold = 2
    }
}

What am I not understanding here?

You don’t need to use id: \.self since your Data class conforms to Identifiable. You don’t need an id parameter at all, in fact. Just do

ForEach(data) { item in
    Text(item.name).tag(item.name)
}

In order for an array of something to be used in a ForEach SwiftUI has to have some way to tell one item in the array from another. If the items do not conform to Identifiable, you have to supply an id parameter to tell SwiftUI how to distinguish items. That id has to be of some type that conforms to Hashable. When you use \.self, you are saying “each item in the data I am giving you can be used to identify the item itself” and therefore conforms to Hashable.

Your Data class does not conform to Hashable so you get the error.

The reason why ForEach(0..<data.count, id: \.self) works is because Int conforms to Hashable and 0..<data.count is a Range<Int>.

UUID conforms to Hashable, so using a UUID as an id in a struct or class that conforms to Identifiable (which requires a property called id of any type that is Hashable) satisfies the criteria for the id parameter of ForEach.

Also, you really should not name structs/classes with the same name as a system-provided object. Foundation already has a Data class. If you name your custom class Data, then there is the potential for the system to get confused and/or to introduce bugs that might be hard to track down.

2 Likes

Thanks for that great answer! I totally understand now and I am learning a lot. It was not clear to me before when to use the id and when it’s not required. Thanks again!

1 Like

woohoo that solved my problem too, thanks

thanks for this question, I am having the same issue and I tried the .self and using other “identifiable” properties and still doesn’t work. can you take a look if you have a chance?

import SwiftUI

struct DatePickers: View {
    @EnvironmentObject var model:ZodiacModel
    
    var cSignos: Zodiac
    
    @State public var myBirthDate = Date()
    @State public var otherBirthDate = Date()
    @State public var myAnimal = ""
    @State public var otherAnimal = ""
    
    var body: some View {
        NavigationView {
            VStack {
                Form {
                    Text ("Your Birthdate")
                    DatePicker("", selection: $myBirthDate, displayedComponents: .date)
                        .datePickerStyle(WheelDatePickerStyle())
                }
                
                Form {
                    Text ("Other Person Birthdate")
                    DatePicker("", selection: $otherBirthDate, displayedComponents: .date)
                        .datePickerStyle(WheelDatePickerStyle())
                }
                
                VStack {
                    NavigationLink(destination: bloodSelect()) {
                        Button {
                            //ForEach(cSignos, id: \.self) { item in
                            ForEach(0..<cSignos, id: \.self) { item in
                                let matchResult = ZodiacModel.findAnimals(birthdate1: myBirthDate, birthdate2: otherBirthDate, zodiacArray: item)
                            }
                            
                        } label: {
                            Text("Next")
                        }
                    }
                }
                
                //click button and find user 1 and user 2 blood types
        }
        
    }//end of navigation view
    }
}


struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        DatePickers()
    }
}

this is my view Model

import Foundation

class ZodiacModel: ObservableObject {
    
    @Published var signos = [Zodiac]()
    
    init() {
        
        // Create an instance of data service and get the data
        self.signos = DataService.getLocalData()
    }
    
    //static so I can just call RecipeModel and use dot notation without creating an instance of recipe model
    static func findAnimals(birthdate1: Date, birthdate2: Date, zodiacArray: Zodiac) -> (String, String) {
        var compatible = ""
        var from = zodiacArray.from
        var to = zodiacArray.to
        var myAnimal = ""
        var otherAnimal = ""
        
        
        if birthdate1 >= from && birthdate1 <= to {
            myAnimal  = zodiacArray.animal
        }
        
        if birthdate2 >= from && birthdate2 <= to {
            myAnimal  = zodiacArray.animal
        }
        
        return (myAnimal, otherAnimal)
        
        //determine the other person animal
        //loop through each zodiac element and find my date/animal
        //compare with matches and find the match type
    }
}

this is my model

import Foundation //

class Zodiac: Identifiable, Decodable {
    //COMMMENT
    var id:UUID?
    var from:Date
    var to:Date
    var animal:String
    var type:String
    var personality:String
    var perfectMatch:String
    var complement:String
    var onlyFriends:String
    var average:String
    var notCompatible:Int
    var loveHate:[String]
    
}

@Naticio

In your DataService.getLocalData() are you assigning the id property with a UUID()?

Maybe post your getLocalData() code.

thanks for the fast response!, here it is

//
//  DataService.swift
//  Compatible
//
//  Created by Nat-Serrano on 5/13/21.
//

import Foundation

class DataService {
    
    static func getLocalData() -> [Zodiac] {
        
        // Parse local json file
        
        // Get a url path to the json file
        let pathString = Bundle.main.path(forResource: "Zodiac", ofType: "json")
        
        // Check if pathString is not nil, otherwise...
        guard pathString != nil else {
            return [Zodiac]()
        }
        
        // Create a url object
        let url = URL(fileURLWithPath: pathString!)
        
        do {
            // Create a data object
            let data = try Data(contentsOf: url)
            
            // Decode the data with a JSON decoder
            let decoder = JSONDecoder()
            
            do {
                
                let zodiacData = try decoder.decode([Zodiac].self, from: data)
                
                // Add the unique IDs
                for r in zodiacData {
                    r.id = UUID()
                }
                
                // Return the recipes
                return zodiacData
            }
            catch {
                // error with parsing json
                print(error)
            }
        }
        catch {
            // error with getting data
            print(error)
        }
        
        return [Zodiac]()
    }
    
}

OK, no problem there.

The issue looks to be in this code:

 VStack {
     NavigationLink(destination: bloodSelect()) {
         Button {
             //ForEach(cSignos, id: \.self) { item in
             ForEach(0..<cSignos, id: \.self) { item in
                 let matchResult = ZodiacModel.findAnimals(birthdate1: myBirthDate, birthdate2: otherBirthDate, zodiacArray: item)
             }

         } label: {
             Text("Next")
         }
     }
 }

You have a Button inside a NavigationLink and then a ForEach inside the Button. What exactly are you trying to do at this point?

Not only that, but the ForEach makes no sense. Using 0..<cSignos implies that cSignos is an Int but it’s actually an instance of the Zodiac class. So, essentially, it’s trying to loop from 0 to Zodiac. Which clearly won’t work.

Several other cases of that kind of name mismatch going on here too. For instance:

static func findAnimals(birthdate1: Date, birthdate2: Date, zodiacArray: Zodiac) -> (String, String) {

The third parameter is called zodiacArray but the type is only a single Zodiac object, not an array.

Disclaimer: I’m new with SwiftUI, so bare with me

Button action:
I am trying to loop through all the elements of the Json, find their corresponding chinese zodiac animal (func findAnimals) and then show the results in another view.

Not only that, but the ForEach makes no sense. Using 0..<cSignos implies that cSignos is an Int but it’s actually an instance of the Zodiac class. So, essentially, it’s trying to loop from 0 to Zodiac . Which clearly won’t work.

yes I agree, I was copying and pasting from online solutions

The third parameter is called zodiacArray but the type is only a single Zodiac object, not an array.

I have seen Chris the teacher do this, use a foreach to loop through all the elements and then call a function inside each loop using dot notation.

I thought this was a community to help students?

@Naticio

We will try our best to help you. I’m not sure what has been said that makes you think otherwise.

What have you done since to try and resolve this code?:

 VStack {
     NavigationLink(destination: bloodSelect()) {
         Button {
             //ForEach(cSignos, id: \.self) { item in
             ForEach(0..<cSignos, id: \.self) { item in
                 let matchResult = ZodiacModel.findAnimals(birthdate1: myBirthDate, birthdate2: otherBirthDate, zodiacArray: item)
             }

         } label: {
             Text("Next")
         }
     }
 }

Do you want to list each Zodiac item and when you select one present another view in which you match the Zodiac with an animal?