问题描述
在使用ForEach,通过id进行标识时:
ForEach(expense.items,id: \.self) { item in
Text(item.name)
}
输出下面的报错:
Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'ExpenseItem' conform to 'Hashable'
经排查发现问题为,我的expense内的ExpenseItem结构没有实现Hashable协议,因此,我无法通过id进行标识每一个参数:
struct ExpenseItem {
var name: String
var type: String
var amount: Double
}
@Observable
class Expenses {
var items = [ExpenseItem]()
}
解决方案
解决方案1
给ExpenseItem实现Hashable协议。因此,应该将ExpenseItem修改为:
struct ExpenseItem: Hashable {
var name: String
var type: String
var amount: Double
}
这里我们需要知道,如果要在ForEach中使用id:\.self作为标识符,就必须要让ForEach的对象遵循Hashable协议,只有Hashable协议可以让我们的对象被唯一的哈希化。
解决方案2
另一个解决方案就是,如果你的类型没有实现Hashable协议,那么就不能使用id:\.self,但是我们可以指定其他的唯一标识符。比如我们这个ExpenseItem结构的name属性:
ForEach(expense.items, id: \.name) { item in
Text(item.name)
}
但是这里明确需要注意的是,我们指定其他的唯一标识符时,对应的标识符在需要是唯一的,否则可能会存在删除错误的情况。
当然,这个删除错误的情况没有在实际中浮现,因此建议遵循这一要求。
解决方案3
方案3为,让ExpenseItem结构遵循Identifiable协议,并设置一个id字段并生成一个UUID对象。
struct ExpenseItem: Identifiable {
var id = UUID()
let name: String
let type: String
let amount: Double
}
最后在ForEach当中,我们就不再需要手动指定id标识,直接使用ForEach:
ForEach(expense.items) { item in
Text(item.name)
}
同类问题
当存在Swift的某个类型(例如Image)类型不支持Hashable协议时:
struct ContentView: View {
@State private var selectedItems: [PhotosPickerItem] = []
@State private var images: [Image] = []
VStack {
ForEach(images,id:\.self) { image in // Referencing initializer 'init(_:id:content:)' on 'ForEach' requires that 'Image' conform to 'Hashable'
image
.resizable()
.scaledToFit()
}
}
}
问题还是跟ForEach的id参数需要一个唯一标识符,它要求列表的元素类型(这里是 Image)符合 Hashable 协议。但 SwiftUI 的 Image 类型并不符合 Hashable 协议,因此会报错。
解决方法
解决方法1
使用索引作为唯一标识符
可以将 images 的索引作为 id 参数:
ForEach(Array(images.enumerated()), id: \.offset) { _, image in
image
.resizable()
.scaledToFit()
}
这里的 Array(images.enumerated()) 将每个图片与其索引配对,id: \.offset 使用索引作为唯一标识符。
enumerated() 返回一个 EnumeratedSequence,它是一个包含元组的序列。每个元组的格式为:
offset:当前元素的索引(从 0 开始)。
element:当前的元素值。
元组的类型是 (offset: Int, element: Element)。
所以可以将offset直接用于唯一标识符。
解包时,则对应的是索引和元素值:index, element:
{ _, image in
image
.resizable()
.scaledToFit()
}
如果不需要显示索引,可以使用_进行替代。
替代方法:
ForEach(Array(images.enumerated()),id:\.0) { _,image in
image
.resizable()
.scaledToFit()
}
使用 \.0 访问元组的第一个元素,offset。
注意:如果数组的元素类型是Hashable(如String,Int,自定义遵循Hashable类型),则可以使用element作为唯一值:
let names = ["Alice", "Bob", "Charlie"]
ForEach(Array(names.enumerated()), id: \.element) { _, name in
Text(name)
}
这里的数组元素 String 遵循 Hashable。
使用 id: \.element 是合法的,因为 String 可以被唯一标识。
解决方法2
将 Image 转换为其他可 Hashable 的类型
如果改为存储图片的基础数据类型(例如 Data 或 UIImage),这些类型都支持 Hashable,然后在显示时将其转换为 Image:
@State private var imageDatas: [Data] = []
ForEach(imageDatas, id: \.self) { data in
if let uiImage = UIImage(data: data) {
Image(uiImage: uiImage)
.resizable()
.scaledToFit()
}
}
解决方法3
引入唯一标识符
为每个图片生成一个唯一标识符并存储在数组中,比如用元组存储图片和标识符:
@State private var images: [(id: UUID, image: Image)] = []
ForEach(images, id: \.id) { item in
item.image
.resizable()
.scaledToFit()
}
当加载图片时,可以为每张图片分配一个 UUID:
images.append((id: UUID(), image: Image(uiImage: uiImage)))
推荐解决方案
使用 方法 3 是最佳选择。UUID 确保了每张图片都有一个独立且唯一的标识符,同时保持代码的灵活性和扩展性。
@State private var images: [(id: UUID, image: Image)] = []
ForEach(images, id: \.id) { item in
item.image
.resizable()
.scaledToFit()
}