update: add same error about Hashable
I have created an Identifiable
compliant protocol and compliant structures. Then, when I create the list and reference it in ForEach
, I get the error Type 'any TestProtocol' cannot conform to 'Identifiable'
(I get the same error about Hashable
).
How should I fix this program?
If I write ForEach(list, id: \.id)
, it works, but I don't think it makes sense to be Identifiable compliant.
import SwiftUI
protocol TestProtocol: Identifiable, Hashable {
var id: UUID { get set }
var name: String { get set }
func greeting() -> String
static func == (lhs: Self, rhs: Self) -> Bool
}
extension TestProtocol {
static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.id == rhs.id
}
}
struct Person: TestProtocol {
var id = UUID()
var name: String
func greeting() -> String {
return "my name is \(name) and I'm a human."
}
}
struct Dog: TestProtocol {
var id = UUID()
var name: String
func greeting() -> String {
return "my name is \(name) and I'm a dog."
}
}
struct ContentView: View {
var list: [any TestProtocol] = [Person(name: "p1"), Dog(name: "d1")]
@State var selected: any TestProtocol
var body: some View {
VStack {
Picker(selection: $selected) { // Type 'any TestProtocol' cannot conform to 'Hashable'
ForEach(list) { l in // Type 'any TestProtocol' cannot conform to 'Identifiable'
Text(l.greeting()).tag(l) // Type 'any TestProtocol' cannot conform to 'Hashable'
}
} label: {
Text("select")
}
}
}
}
Your error message complaining about Hashable
is a "red hering". The protocol TestProtocol
, and therefor all structs conforming to it, conforms to Hashable
.
let person = Person(name: "IAmHashable")
print(person.hashValue)
The reason this is failing is within the Picker
. It needs a concrete type and not a protocol. One solution would be to create a "Container" type and a custom binding that handles this.
struct Container: Identifiable, Hashable{
//implement the same equality as in your TestProtocol
static func == (lhs: Container, rhs: Container) -> Bool {
rhs.wrapped.id == lhs.wrapped.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(wrapped)
}
var wrapped: any TestProtocol
//for convenience
var id: UUID {wrapped.id}
}
and the ContentView:
struct ContentView: View {
let startArr: [any TestProtocol] = [Person(name: "p1"), Dog(name: "d1")]
@State private var selected: (any TestProtocol)?
var body: some View {
// list of wrapped protocols
var list: [Container] = { startArr.map{Container(wrapped: $0)}}()
// binding
let selectionBinding: Binding<Container> = .init {
let returninstance = list.first { cont in
cont.id == selected?.id
}
return returninstance ?? list[0]
} set: { container in
selected = container.wrapped
}
// viewCode
VStack {
Picker(selection: selectionBinding) {
ForEach(list) { l in
Text(l.wrapped.greeting())
.tag(l)
}
} label: {
Text("select")
}
// confirmation selection changed
Text(selected?.name ?? "no Selection")
}
}
}
This solution has a few drawbacks :
startArr
should never be empty, else return returninstance?? list[0]
return returninstance?? list[0]
will break you code. This can be handled, but I think this is out of the scope of this question.Container
needs to be the same as in TestProtocol
as you cannot compare two any Protocol
s ContainerView
selected
will be nil until something is selected. Any usage, eg: the Text
element in this case, needs to deal with this. But you could probably set this in .onApear
.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.