SwiftUI Picker — warum Menu das bessere Label hat

Ursprünglich erschienen auf Medium: Setup SwiftUI Picker with a label (Juli 2024) und Create Generics Enum PickerView (Januar 2025).

Der SwiftUI Picker mit .inline-Style hat ein eigenartiges Problem: Man gibt ihm ein Label, und er zeigt stattdessen ein Chevron.


Das Problem ist bekannt und ärgerlich. PickerStyle.inline ignoriert das Label komplett und zeigt statt der Beschriftung nur einen einsamen Pfeil ohne jeden Kontext. Apple’s eigene Dokumentation schweigt dazu.

Picker(selection: $gender) {
    Text(Gender.male.rawValue).tag(Gender.male)
    Text(Gender.female.rawValue).tag(Gender.female)
    Text(Gender.nonbinary.rawValue).tag(Gender.nonbinary)
} label: {
    Text("Select your Gender")
}

Picker mit inline Style zeigt nur ein Chevron statt dem Label

Das Label wird deklariert, aber nie angezeigt. Der User sieht nur das Chevron und muss raten, wofür es steht.


Die Lösung: Menu als Wrapper

Wer ein Menü will, sollte tatsächlich auch ein Menü verwenden. Der Menu-View nimmt den Picker in seinen Content, und das Label lässt sich dabei frei gestalten.

Menu {
    Picker("", selection: $gender) {
        ForEach(Gender.allCases) { data in
            Text(data.rawValue).tag(data)
        }
    }
    .pickerStyle(.inline)
} label: {
    HStack {
        Text(gender.rawValue)
            .font(.title3)
            .frame(maxWidth: 200)
        Image(systemName: "chevron.up.chevron.down")
            .padding(.trailing)
    }
    .frame(maxWidth: .infinity)
    .frame(height: 55)
    .background(.white)
    .cornerRadius(10)
    .accentColor(.pink)
}
AspektInline-PickerMenu-Wrapper
Label sichtbarNein, nur ein ChevronJa, und frei gestaltbar
Klickbare FlächeNur das Chevron selbstDer gesamte Button-Bereich
StylingStark eingeschränktVolle SwiftUI-Modifier

Picker Label und der komplette Button-Bereich sind anklickbar

Der gesamte Button-Bereich ist jetzt klickbar, das Label zeigt den aktuellen Wert, und das Menü öffnet sich sauber als Inline-Sheet.

Das Inline-Menü öffnet sich nach Klick auf den gestylten Bereich


Bonus: Generischer Enum-Picker

Bei drei Pickern im selben Formular wird der Code schnell redundant. Die Lösung ist ein generischer PickerView, der mit jedem konformen Enum funktioniert.

Das Protokoll

protocol PickerEnum: Hashable, CaseIterable {
    var stringValue: String { get }
}

Die View

struct GenericPickerView<S: PickerEnum, Label: View, Content: View>: View {
    let selection: Binding<S>
    let label: Label
    @ViewBuilder var content: (S) -> Content

    var body: some View {
        Picker(selection: selection, label: label) {
            ForEach(S.allCases, id: \.self) {
                content($0)
            }
        }
    }
}

Verwendung

List {
    GenericPickerView(
        selection: $direction,
        label: Text("Direction"),
        content: { Text("\($0.stringValue.capitalized)").tag($0) }
    )
    GenericPickerView(
        selection: $orientation,
        label: Text("Orientation"),
        content: { Text("\($0.stringValue.capitalized)").tag($0) }
    )
}

Generischer Picker mit Direction und Orientation Enum

Ausgewählte Werte im generischen Picker

Zwei Picker, null Redundanz. Neues Enum dazu? Einfach zu PickerEnum konformieren, fertig.

Die RandomAccessCollection-Falle

CaseIterable.AllCases konformiert nicht automatisch zu RandomAccessCollection, was beim Verwenden in ForEach zu einem Compiler-Fehler führt. Es gibt drei saubere Wege, das zu lösen:

VarianteBewertung
ForEach(Array(S.allCases), id: \.self)Funktioniert, erzeugt aber bei jedem Render einen Array-Cast
where S.AllCases == Array<S> am StructExplizit und sicher
where AllCases: RandomAccessCollection am ProtokollEinmal definiert, überall gültig

Die dritte Variante ist die sauberste, weil die Constraint direkt am Protokoll definiert wird und nicht an jeder View einzeln wiederholt werden muss.


Zwei Patterns, die in jedes SwiftUI-Projekt gehören: Menu statt Inline-Picker und Generics statt Copy-Paste.