18
votes

Sélecteur de type de données facultatif dans SwiftUI?

Normalement, je peux afficher une liste d'éléments comme celui-ci dans SwiftUI:

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

Cela fonctionne parfaitement, me permettant de sélectionner le fruit que je veux. Si je veux changer de fruit pour qu'il soit nullable (c'est-à-dire facultatif), cela pose des problèmes:

enum Fruit {
    case apple
    case orange
    case banana
}

struct FruitView: View {

    @State private var fruit = Fruit.apple

    var body: some View {
        Picker(selection: $fruit, label: Text("Fruit")) {
            ForEach(Fruit.allCases) { fruit in
                Text(fruit.rawValue).tag(fruit)
            }
        }
    }
}

Le nom du fruit sélectionné n'est plus affiché sur le premier écran, et quel que soit l'élément de sélection que je choisis, il ne met pas à jour la valeur du fruit.

Comment utiliser Picker avec un type facultatif?


2 commentaires

pouvez-vous partager votre classe / structure Fruit


@SimonBachmann: c'est une énumération, a mis à jour la question


4 Réponses :


36
votes

La balise doit correspondre au type de données exact lors de l'encapsulation de la liaison. Dans ce cas, le type de données fourni pour tag est Fruit mais le type de données $fruit.wrappedValue est Fruit? . Vous pouvez résoudre ce problème en convertissant le type de données dans la méthode de tag :

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?)
            }
        }
    }
}

Bonus : Si vous voulez un texte personnalisé pour nil (au lieu de simplement vide), et voulez que l'utilisateur soit autorisé à sélectionner nil (Remarque: c'est tout ou rien ici), vous pouvez inclure un élément pour nil :

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?)
            }
        }
    }
}

N'oubliez pas de lancer la valeur nil également.


1 commentaires

Quelques variations de cette syntaxe: .tag(nil as Fruit?) , .tag(Fruit?.none) , .tag(Fruit?(nil)) .tag(Optional<Fruit>.none) .tag(Fruit?(nil)) , .tag(Optional<Fruit>.none) . 🙂



-1
votes

Pourquoi ne pas étendre l'énumération avec une valeur par défaut? Si ce n'est pas ce que vous essayez d'atteindre, peut-être pouvez-vous également fournir des informations sur les raisons pour lesquelles vous voulez que cela soit 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)")
        }
    }
}


1 commentaires

Votre commentaire est assez juste pour une enum , mais que se passe-t-il s'il s'agissait d'une sélection à partir d'un tableau ou d'un ensemble où «aucune des réponses ci-dessus» pourrait être un choix valide? La réponse de @ Senseful est alors très utile!



-1
votes

J'ai fait un repo public ici avec la solution de Senseful: https://github.com/andrewthedina/SwiftUIPickerWithOptionalSelection

EDIT: Merci pour les commentaires concernant la publication de liens. Voici le code qui répond à la question. Le copier / coller fera l'affaire, ou clonera le dépôt à partir du lien.

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


3 commentaires

Veuillez ne pas simplement publier des liens vers une réponse. Postez le code correspondant et le lien. De cette façon, votre réponse ne disparaît pas et la valeur qu'elle aurait pu fournir est perdue.


Bien que ce lien puisse répondre à la question, il est préférable d'inclure les parties essentielles de la réponse ici et de fournir le lien pour référence. Les réponses aux liens uniquement peuvent devenir invalides si la page liée change. - De l'avis


J'apprécie cela, messieurs (@Andrew, @kenny_k). Édité!



0
votes

En fait, je préfère la solution de @ Senseful pour une solution ponctuelle, mais pour la postérité: vous pouvez également créer une énumération de wrapper, qui si vous avez une tonne de types d'entités dans votre application, évolue assez bien via des extensions de protocole.

Picker(selection: $task.job, label: Text("Job")) {
    ForEach(Model.shared.jobs.optionalPickable) { p in
        p.itemView
    }
}

L'utilisation est alors assez serrée:

// 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)
    }
}


0 commentaires