iosswiftuiswiftui-picker

SwiftUI Form Picker options are not selecting


Why can't I select items from this picker view? The difference here is my selection array consist two different Structs('CustomOptionOne' and 'CustomOptionTwo') that conforms to the same protocol 'PickerOption'. The compiler gives me two errors,

Type 'any PickerOption' cannot conform to 'Hashable'

Static method 'buildExpression' requires that 'Content' conform to 'AccessibilityRotorContent'

How can I achieve this?

Any help would be greatly appreciated.

protocol PickerOption: Hashable, Identifiable {
    var name: String { get set }
    var description: String { get set }
    var id: String { get }
}

struct MyView: View {
    
    //I know it's this line, but my array will consist of any object that conforms to PickerOption protocol
    private static var exampleStructs: [any PickerOption] = [
        CustomOptionOne(name: "Example 1", description: "Example Name 1", id: "5"),
        CustomOptionOne(name: "Example 2", description: "Example Name 2", id: "6"),
        CustomOptionTwo(name: "Example 3", description: "Example Name 3", id: "7"),
        CustomOptionTwo(name: "Example 4", description: "Example Name 4", id: "8")
    ]
    
    private struct CustomOptionOne: Hashable, Identifiable, PickerOption {
        var name: String
        var description: String
        var id: String
    }
    
    private struct CustomOptionTwo: Hashable, Identifiable, PickerOption {
        var name: String
        var description: String
        var id: String
    }
    
    @State private var selectedStruct: any PickerOption = CustomOptionOne(name: "Example Name", description: "Example Description", id: "9")
    
    var body: some View {
        
        NavigationView {
            Form {
                Section(header: Text("Selected Struct")) {
                    Text(selectedStruct.name)
                    Text(selectedStruct.description)
                }
                
                Section(header: Text("Picker")) {
                    Picker(selection: $selectedStruct, label: Text(selectedStruct.name), content: {
                        ForEach(MyView.exampleStructs, id: \.id) { exampleStruct in
                            Text(exampleStruct.name).tag(exampleStruct)
                        }
                    })
                }
            }
        }
    }
}

I've checked many answers including this swift picker not selecting item and none of them are answering my use case scenario.


Solution

  • For a Picker the tag and the selection must be of the exact same type your specific example adds an additional layer of complexity because you are trying to use existentials (any) with SwiftUI.

    So the simplest solution is to use the ID for both the selection and the tag but first you have to restrict the ID which comes from Identifiable to a specific type so you can match that for the Picker

    protocol PickerOption: Hashable, Identifiable where ID == String {
        var name: String { get set }
        var description: String { get set }
        var id: String { get }
    }
    

    Then change the property to use a String

    @State private var selectedStructId: String? = nil
    

    Then change the tag to match the selectedStructId

    Text(exampleStruct.name).tag(exampleStruct.id as String?)
    

    The whole thing put together will look something like

    struct ContentView: View {
        
        //I know it's this line, but my array will consist of any object that conforms to PickerOption protocol
        private static var exampleStructs: [any PickerOption] = [
            CustomOptionOne(name: "Example 1", description: "Example Name 1", id: "5"),
            CustomOptionOne(name: "Example 2", description: "Example Name 2", id: "6"),
            CustomOptionTwo(name: "Example 3", description: "Example Name 3", id: "7"),
            CustomOptionTwo(name: "Example 4", description: "Example Name 4", id: "8")
        ]
        
        private struct CustomOptionOne: PickerOption {
            var name: String
            var description: String
            var id: String
        }
        
        private struct CustomOptionTwo: PickerOption {
            var name: String
            var description: String
            var id: String
        }
        
        @State private var selectedStructId: String? = nil
        
        var body: some View {
            NavigationStack {
                Form {
                    if let selectedStructId, let selectedStruct = ContentView.exampleStructs.filter({ o in
                        o.id == selectedStructId
                    }).first{ //Find the selected selected struct
                        Section(header: Text("Selected Struct")) {
                            Text(selectedStruct.name)
                            Text(selectedStruct.description)
                        }
                    }
                    Section(header: Text("Picker")) {
                        Picker(selection: $selectedStructId, label: Text(""), content: {
                            Text("Select an object.")
                            ForEach(ContentView.exampleStructs, id: \.id) { exampleStruct in
                                Text(exampleStruct.name).tag(exampleStruct.id as String?)
    
                            }
                        })
                    }
                }
            }
        }
    }
    

    I know this isn't ideal but existentials are an uphill battle with SwiftUI.