Picker for optional data type in SwiftUI?

后端 未结 4 1063
夕颜
夕颜 2021-02-12 16:13

Normally I can display a list of items like this in SwiftUI:

enum Fruit {
    case apple
    case orange
    case banana
}

struct FruitView: View {

    @State          


        
相关标签:
4条回答
  • 2021-02-12 16:57

    Why not extending the enum with a default value? If this is not what you are trying to achieve, maybe you can also provide some information, why you want to have it optional.

    enum Fruit: String, CaseIterable, Hashable {
        case apple = "apple"
        case orange = "orange"
        case banana = "banana"
        case noValue = ""
    }
    
    struct ContentView: View {
    
        @State private var fruit = Fruit.noValue
    
        var body: some View {
            VStack{
                Picker(selection: $fruit, label: Text("Fruit")) {
                    ForEach(Fruit.allCases, id:\.self) { fruit in
                        Text(fruit.rawValue)
                    }
                }
                Text("Selected Fruit: \(fruit.rawValue)")
            }
        }
    }
    
    0 讨论(0)
  • 2021-02-12 17:01

    The tag must match the exact data type as the binding is wrapping. In this case the data type provided to tag is Fruit but the data type of $fruit.wrappedValue is Fruit?. You can fix this by casting the datatype in the tag method:

    struct FruitView: View {
    
        @State private var fruit: Fruit?
    
        var body: some View {
            Picker(selection: $fruit, label: Text("Fruit")) {
                ForEach(Fruit.allCases) { fruit in
                    Text(fruit.rawValue).tag(fruit as Fruit?)
                }
            }
        }
    }
    

    Bonus: If you want custom text for nil (instead of just blank), and want the user to be allowed to select nil (Note: it's either all or nothing here), you can include an item for nil:

    struct FruitView: View {
    
        @State private var fruit: Fruit?
    
        var body: some View {
            Picker(selection: $fruit, label: Text("Fruit")) {
                Text("No fruit").tag(nil as Fruit?)
                ForEach(Fruit.allCases) { fruit in
                    Text(fruit.rawValue).tag(fruit as Fruit?)
                }
            }
        }
    }
    

    Don't forget to cast the nil value as well.

    0 讨论(0)
  • 2021-02-12 17:01

    I made a public repo here with Senseful's solution: https://github.com/andrewthedina/SwiftUIPickerWithOptionalSelection

    EDIT: Thank you for the comments regarding posting links. Here is the code which answers the question. Copy/paste will do the trick, or clone the repo from the link.

    import SwiftUI
    
    struct ContentView: View {
        @State private var selectionOne: String? = nil
        @State private var selectionTwo: String? = nil
        
        let items = ["Item A", "Item B", "Item C"]
        
        var body: some View {
            NavigationView {
                Form {
                    // MARK: - Option 1: NIL by SELECTION
                    Picker(selection: $selectionOne, label: Text("Picker with option to select nil item [none]")) {
                        Text("[none]").tag(nil as String?)
                            .foregroundColor(.red)
    
                        ForEach(items, id: \.self) { item in
                            Text(item).tag(item as String?)
                            // Tags must be cast to same type as Picker selection
                        }
                    }
                    
                    // MARK: - Option 2: NIL by BUTTON ACTION
                    Picker(selection: $selectionTwo, label: Text("Picker with Button that removes selection")) {
                        ForEach(items, id: \.self) { item in
                            Text(item).tag(item as String?)
                            // Tags must be cast to same type as Picker selection
                        }
                    }
                    
                    if selectionTwo != nil { // "Remove item" button only appears if selection is not nil
                        Button("Remove item") {
                            self.selectionTwo = nil
                        }
                    }
                }
            }
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    
    0 讨论(0)
  • 2021-02-12 17:15

    I actually prefer @Senseful's solution for a point solution, but for posterity: you could also create a wrapper enum, which if you have a ton of entity types in your app scales quite nicely via protocol extensions.

    // utility constraint to ensure a default id can be produced
    protocol EmptyInitializable {
        init()
    }
    
    // primary constraint on PickerValue wrapper
    protocol Pickable {
        associatedtype Element: Identifiable where Element.ID: EmptyInitializable
    }
    
    // wrapper to hide optionality
    enum PickerValue<Element>: Pickable where Element: Identifiable, Element.ID: EmptyInitializable {
        case none
        case some(Element)
    }
    
    // hashable & equtable on the wrapper
    extension PickerValue: Hashable & Equatable {
        func hash(into hasher: inout Hasher) {
            hasher.combine(id)
        }
        
        static func ==(lhs: Self, rhs: Self) -> Bool {
            lhs.id == rhs.id
        }
    }
    
    // common identifiable types
    extension String: EmptyInitializable {}
    extension Int: EmptyInitializable {}
    extension UInt: EmptyInitializable {}
    extension UInt8: EmptyInitializable {}
    extension UInt16: EmptyInitializable {}
    extension UInt32: EmptyInitializable {}
    extension UInt64: EmptyInitializable {}
    extension UUID: EmptyInitializable {}
    
    // id producer on wrapper
    extension PickerValue: Identifiable {
        var id: Element.ID {
            switch self {
                case .some(let e):
                    return e.id
                case .none:
                    return Element.ID()
            }
        }
    }
    
    // utility extensions on Array to wrap into PickerValues
    extension Array where Element: Identifiable, Element.ID: EmptyInitializable {
        var pickable: Array<PickerValue<Element>> {
            map { .some($0) }
        }
        
        var optionalPickable: Array<PickerValue<Element>> {
            [.none] + pickable
        }
    }
    
    // benefit of wrapping with protocols is that item views can be common
    // across data sets.  (Here TitleComponent { var title: String { get }})
    extension PickerValue where Element: TitleComponent {
        @ViewBuilder
        var itemView: some View {
            Group {
                switch self {
                    case .some(let e):
                        Text(e.title)
                    case .none:
                        Text("None")
                            .italic()
                            .foregroundColor(.accentColor)
                }
            }
            .tag(self)
        }
    }
    

    Usage is then quite tight:

    Picker(selection: $task.job, label: Text("Job")) {
        ForEach(Model.shared.jobs.optionalPickable) { p in
            p.itemView
        }
    }
    
    0 讨论(0)
提交回复
热议问题