iosswiftswiftuiswiftui-list

Make Swift existential 'any' protocol conform to Hashable


I'm having some trouble getting my head around conforming existential variables in Swift 5.7+ with the new "existential any" changes.

Since Animal here will always conform to Hashable, I would imagine that any Animal would also have to conform to Hashable.

However, the error I'm seeing is causing me to think otherwise. Can someone help explain why this is occurring, and if possible, help me resolve it?

import SwiftUI
import PlaygroundSupport

protocol Animal: Hashable {
    var name: String { get }
}

struct Cow: Animal {
    let name: String
}

struct Chicken: Animal {
    let name: String
}

let anAnimalList: [any Animal] = [Cow(name: "Aaron"), Chicken(name: "Billy"), Cow(name: "Charlie"), Chicken(name: "Delilah")]

struct myView: View {
    @State private var anAnimal: (any Animal)?
    
    var body: some View {
        VStack {
            List(anAnimalList, id: \.self, selection: $anAnimal) { animal in // ERROR: Type 'any Animal' cannot conform to 'Hashable'
                Text("Animal")
            }
            
            Text("Animal is \(anAnimal?.name ?? "Null")")
        }
    }
}

PlaygroundPage.current.setLiveView(myView())


Solution

  • Assuming we want to find a way of how to list [any Animal] but not "why this shit happens" :) (see corresponding WWDC22 session to find answer on second one)

    Tested with Xcode 14b3 / iOS 16

    demo

    Let's jump into this hole...

    There are two places of reported error in original code:

    List(anAnimalList, 
                       id: \.self,     // << 1st place
                selection: $anAnimal   // << 2nd place
    ) { animal in
        Text("Animal")
    }
    

    1st place - because Animal is not identifiable and self relates to instance of concrete type, so we should not use \.self (because compiler will not be able to infer concrete type), but identify directly by \.id

    So here is a possible fix for the first issue:

    protocol Animal: Hashable, Identifiable {
        var id: UUID { get }
        var name: String { get }
    }
    
    struct Cow: Animal {
        let id = UUID()
        let name: String
    }
    
    struct Chicken: Animal {
        let id = UUID()
        let name: String
    }
    
        var body: some View {
            VStack {
                List(anAnimalList, id: \.id) { animal in   // << no error !!
                    Text("Animal: \(animal.name)")
                }
            }
        }
    
    

    2nd place - now about selection; the story is the same, swift should know "concrete type of selection" or could be able "infer it from code", both are impossible at compile type, so instead trying to break the wall with a head, let's find a door...

    A possible solution is to use for selection something known and concrete in the context... we've just added it above - the 'id'

    So here a fix for second part

    @State private var anAnimalID: UUID? // << here !!
    
    var body: some View {
        VStack {
            List(anAnimalList, id: \.id, selection: $anAnimalID) { animal in
                Text("Animal: \(animal.name)")
            }
            if let animal = anAnimalList.first { $0.id == anAnimalID } {
                Text("Animal is \(animal.name)")
            }
        }
    }
    

    Test code on GitHub