I'm trying to build a reusable component that includes a SwiftUI Picker that can work with different types in several places in my app. I created a Pickable protocol that conforms to Hashable, but when I try to use it, the Picker and the ForEach complain that Type 'any Pickable' cannot conform to 'Hashable'
import SwiftUI
struct PickerRow: View {
let title: String
let options: [any Pickable]
@State var selection: any Pickable
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subHeading)
Picker(title, selection: $selection, content: {
ForEach(options, id: \.self) {
Text($0.name)
}
}).pickerStyle(.menu)
}
}
}
protocol Pickable: Hashable {
var name: String { get }
}
Is there a way to get something like this to work without specifying a concrete type?
CodePudding user response:
If you think about it, it makes sense.
What would you expect to happen if that code was valid and you used it like this?
struct ContentView: View {
let options = [PickableA(), PickableB()]
@State var selection = PickableC()
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}
That couldn't possibly work right?
What you need is a way to make sure that there is a constraint that forces options
and selection
to be of the same concrete type (consider Equatable
for example, both String
and Int
are conforming, but you cannot compare them).
One possible solution would be a generic constraint in the declaration of your struct (also note the @Binding
instead of @State
since we modify external values):
struct PickerRow<Option: Pickable>: View {
let title: String
let options: [Option]
@Binding var selection: Option
var body: some View {
HStack {
Spacer()
Text(title)
.font(.subheadline)
Picker(title, selection: $selection) {
ForEach(options, id: \.self) {
Text($0.name)
}
}.pickerStyle(.menu)
}
}
}
which you could use like this:
struct Person: Pickable {
let name: String
}
struct ContentView: View {
let options = [Person(name: "Bob"), Person(name: "Alice")]
@State var selection = Person(name: "Bob")
var body: some View {
PickerRow(title: "Choose one", options: options, selection: $selection)
}
}