I’m working on a SwiftUI project where I have a Target protocol that conforms to Identifiable and Hashable. I have two structs, Silhouette and IPSC, that conform to this protocol. I want to display a Picker in my ContentView where users can select one of these targets.
Here is my code:
import SwiftUI
protocol Target: Identifiable, Hashable {
var id: UUID { get }
var name: String { get }
var description: String { get }
}
struct Silhouette: Target {
let id = UUID()
var name: String = "Silhouette"
var description: String = "A normal target"
}
struct IPSC: Target {
let id = UUID()
var name: String = "IPSC"
var description: String = "A special target"
}
struct ContentView: View {
private let targets: [any Target] = [Silhouette(), IPSC()]
@State private var selectedTarget: any Target = Silhouette()
var body: some View {
Picker("Targets", selection: $selectedTarget) {
ForEach(targets, id: \.id) { target in
Text(target.name)
}
}
}
}
However, this code doesn’t compile and gives the following error:
Type 'any Target' cannot conform to 'Hashable'
From my understanding, both Silhouette and IPSC conform to Hashable, so why does the compiler complain about this? How can I fix this issue?
I would appreciate any help or guidance on how to resolve this problem while keeping the protocol-based structure.
When you declare any Target
(an existential type), it means “an instance of some type conforming to the Target
protocol”.
The Hashable conformance is tied to specific concrete types (not existential types), and Swift cannot guarantee the existential any Target can be hashed just because the underlying type can be hashed - since it has no knowledge of the concrete type at runtime.
The problem here is that targets
array uses any Target
, which erases type-specific details. This makes it hard for Swift to enforce Hashable and Identifiable guarantees, even though both Silhouette and IPSC are Hashable
.
If you want to stick to this approach instead of using an enum as suggested by others, you can fix these errors by using a type-erased wrapper:
struct AnyTarget: Target {
let id: UUID
let name: String
let description: String
init<T: Target>(_ target: T) {
self.id = target.id
self.name = target.name
self.description = target.description
}
}
Here's the full code:
import SwiftUI
protocol Target: Identifiable, Hashable {
var id: UUID { get }
var name: String { get }
var description: String { get }
}
//Type-erased wrapper
struct AnyTarget: Target {
let id: UUID
let name: String
let description: String
init<T: Target>(_ target: T) {
self.id = target.id
self.name = target.name
self.description = target.description
}
}
struct Silhouette: Target {
let id = UUID()
var name: String = "Silhouette"
var description: String = "A normal target"
}
struct IPSC: Target {
let id = UUID()
var name: String = "IPSC"
var description: String = "A special target"
}
struct PickerAnyTarget: View {
private let targets: [AnyTarget] = [AnyTarget(Silhouette()), AnyTarget(IPSC())]
@State private var selectedTarget: AnyTarget?
var body: some View {
Picker("Targets", selection: $selectedTarget) {
Text("Select a target").tag(nil as AnyTarget?) // <- placeholder before selection - optional
ForEach(targets, id: \.id) { target in
Text(target.name).tag(target as AnyTarget?) // <- tag is required for actual selection to work - needs to match the type of selectedTargeted
}
}
}
}
#Preview {
PickerAnyTarget()
}