Xcode报错:Referencing initializer ‘init(_:id:content:)’ on ‘ForEach’ requires that ‘ExpenseItem’ conform to ‘Hashable’
Xcode报错:Referencing initializer ‘init(_:id:content:)’ on ‘ForEach’ requires that ‘ExpenseItem’ conform to ‘Hashable’

Xcode报错:Referencing initializer ‘init(_:id:content:)’ on ‘ForEach’ requires that ‘ExpenseItem’ conform to ‘Hashable’

问题描述

在使用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()
}

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注