swiftswiftuiprotocolspickerhashable

Type ‘any Target’ cannot conform to ‘Hashable’ in SwiftUI Picker


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.


Solution

  • 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()
    }