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
}
}
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.
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!
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]
}
//
// 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]()
}
}
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:
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.