Lektion 2.13 · Stufe 2 – SwiftUI-Einstieg

Picker, Slider, Stepper, DatePicker

ca. 15 Minuten Lesezeit

Lernziel

Nach dieser Lektion kannst du die wichtigsten Auswahl-Elemente einsetzen: Picker in verschiedenen Stilen, Slider, Stepper, DatePicker und ColorPicker. Du weißt, welches Element für welche Art von Auswahl passt, und kannst Picker mit Enums kombinieren.

Das Problem: Tippen ist nicht immer die beste Eingabe

Eine Kaffeebestellung: Größe, Milchsorte, Anzahl, Abholzeit. Mit Textfeldern wäre das mühsam und fehleranfällig. Wer weiß schon, ob er „Hafer“, „Hafermilch“ oder „hafer“ tippen soll?

Für Auswahlen aus festen Möglichkeiten, Zahlen in einem Bereich und Zeitpunkte gibt es eigene Bedienelemente. Sie sind schneller, verhindern ungültige Eingaben und fühlen sich vertraut an, weil sie überall in iOS verwendet werden.

Picker: Eine aus mehreren

Swift
struct CoffeeView: View {
    @State private var size = "M"

    var body: some View {
        VStack(spacing: 20) {
            Picker("Größe", selection: $size) {
                Text("Klein").tag("S")
                Text("Mittel").tag("M")
                Text("Groß").tag("L")
            }

            Text("Gewählt: \(size)")
        }
    }
}

So funktioniert ein Picker:

  • selection: $size ist ein Binding auf den gewählten Wert (Lektion 2.10).
  • Jede Option ist eine View, hier ein Text.
  • .tag(...) verbindet die Option mit einem Wert. Wählt der Nutzer „Mittel“, wird size auf "M" gesetzt.

Achtung

Der Typ des Tags muss genau zum Typ der Auswahl passen. Ist size ein String, müssen alle Tags Strings sein. Passt das nicht, kann der Nutzer zwar tippen, aber die Auswahl ändert sich nicht, und es gibt keine Fehlermeldung.

Picker mit Enums

Mit Strings als Tags droht wieder das Tippfehler-Problem aus Lektion 1.14. Besser ist ein Enum:

Swift
enum CoffeeSize: String, CaseIterable, Identifiable {
    case small = "Klein"
    case medium = "Mittel"
    case large = "Groß"

    var id: Self { self }
}

struct CoffeeView: View {
    @State private var size = CoffeeSize.medium

    var body: some View {
        Picker("Größe", selection: $size) {
            ForEach(CoffeeSize.allCases) { size in
                Text(size.rawValue)
            }
        }
    }
}

Hier kommt alles aus Stufe 1 zusammen:

  • String als Raw Value liefert den Anzeigetext.
  • CaseIterable liefert mit allCases alle Optionen.
  • Identifiable mit var id: Self { self } macht jeden Fall eindeutig erkennbar, damit ForEach die Optionen auseinanderhalten kann (Lektion 1.15).

Und das Schöne: Bei ForEach über die Fälle eines Enums setzt SwiftUI die Tags automatisch. Kommt eine neue Größe dazu, erscheint sie ohne weitere Änderung im Picker.

ForEach schauen wir uns in Lektion 2.15 genau an. Für jetzt reicht: Es erzeugt für jedes Element eine View.

Picker-Stile

Derselbe Picker kann ganz unterschiedlich aussehen:

Swift
Picker("Größe", selection: $size) { ... }
    .pickerStyle(.segmented)
StilAussehenGut für
.menuKnopf, der ein Menü öffnet (Standard außerhalb von Formularen)viele Optionen, wenig Platz
.segmentednebeneinanderliegende Segmente2 bis 5 kurze Optionen, sofort sichtbar
.wheelDrehradZahlen, Zeiten
.inlinealle Optionen als Liste (in Formularen)wenige Optionen, die man sehen soll
.navigationLinkeigene Seite mit Liste (in Formularen mit Navigation)viele Optionen
.paletteSymbole nebeneinander, z. B. in MenüsFarben, Symbole

Tipp

.segmented ist für Filter und Ansichtsumschaltungen ideal, etwa „Tag / Woche / Monat“. Erinnerst du dich an die Kaffeegrößen-Übung aus Lektion 2.11? Mit einem segmentierten Picker ist das eine Zeile.

Slider: Werte stufenlos wählen

Swift
@State private var volume = 0.5

Slider(value: $volume)

Ohne weitere Angaben geht der Wert von 0 bis 1. Meist willst du einen eigenen Bereich, Schritte und Beschriftungen:

Swift
@State private var temperature = 21.0

VStack {
    Slider(value: $temperature, in: 15...28, step: 0.5) {
        Text("Temperatur")
    } minimumValueLabel: {
        Image(systemName: "snowflake")
    } maximumValueLabel: {
        Image(systemName: "flame")
    }

    Text("\(temperature, format: .number) °C")
}
  • in: 15...28 legt den Bereich fest, mit einem Range aus Lektion 1.4.
  • step: 0.5 lässt den Regler in halben Schritten einrasten.
  • Die Labels links und rechts zeigen die Enden des Bereichs.

Achtung

Slider arbeitet mit Kommazahlen. Bindest du ihn an einen Int, meldet Xcode einen Fehler. Nimm einen Double und runde für die Anzeige.

Stepper: In kleinen Schritten zählen

Swift
@State private var cups = 1

Stepper("Tassen: \(cups)", value: $cups, in: 1...10)

Zwei kleine Knöpfe „−“ und „+“. Mit in: legst du Minimum und Maximum fest, mit step: die Schrittweite. Am Rand werden die Knöpfe automatisch deaktiviert.

Slider oder Stepper?

  • Slider: großer Bereich, ungefähre Werte sind in Ordnung (Lautstärke, Helligkeit).
  • Stepper: kleiner Bereich, genaue Werte zählen (Anzahl, Personen, Wiederholungen).

DatePicker: Datum und Uhrzeit

Swift
@State private var pickupTime = Date.now

DatePicker("Abholung", selection: $pickupTime)

Das zeigt Datum und Uhrzeit als kompakte Knöpfe. Ein Tipp darauf öffnet einen Kalender bzw. ein Zeitrad.

Nur Datum oder nur Uhrzeit:

Swift
DatePicker("Geburtstag", selection: $birthday, displayedComponents: .date)
DatePicker("Wecker", selection: $alarm, displayedComponents: .hourAndMinute)

Einen erlaubten Bereich festlegen:

Swift
DatePicker(
    "Termin",
    selection: $appointment,
    in: Date.now...,
    displayedComponents: [.date, .hourAndMinute]
)

Date.now... ist ein offener Bereich: ab jetzt, ohne Ende. Termine in der Vergangenheit sind damit nicht wählbar. Mit ...Date.now wären nur vergangene Daten erlaubt, gut für ein Geburtsdatum.

Und verschiedene Stile:

Swift
DatePicker("Datum", selection: $date, displayedComponents: .date)
    .datePickerStyle(.graphical)

.graphical zeigt direkt einen ganzen Kalender, .wheel die klassischen Drehräder, .compact (Standard) die platzsparenden Knöpfe.

ColorPicker: Farben wählen

Swift
@State private var favoriteColor = Color.blue

ColorPicker("Lieblingsfarbe", selection: $favoriteColor)

Ein Tipp auf den Farbkreis öffnet Apples Farbauswahl mit Raster, Spektrum und Reglern.

Alles zusammen

Eine kleine Kaffeebestellung. Die Elemente funktionieren auch außerhalb eines Formulars, in einem Form (nächste Lektion) sehen sie aber noch aufgeräumter aus:

Swift
enum Milk: String, CaseIterable, Identifiable {
    case cow = "Kuhmilch"
    case oat = "Hafer"
    case soy = "Soja"
    case none = "Ohne"

    var id: Self { self }
}

struct OrderView: View {
    @State private var size = CoffeeSize.medium
    @State private var milk = Milk.oat
    @State private var cups = 1
    @State private var sweetness = 0.0
    @State private var pickup = Date.now

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Picker("Größe", selection: $size) {
                ForEach(CoffeeSize.allCases) { size in
                    Text(size.rawValue)
                }
            }
            .pickerStyle(.segmented)

            Picker("Milch", selection: $milk) {
                ForEach(Milk.allCases) { milk in
                    Text(milk.rawValue)
                }
            }

            Stepper("Anzahl: \(cups)", value: $cups, in: 1...10)

            VStack(alignment: .leading) {
                Text("Süße")
                Slider(value: $sweetness, in: 0...3, step: 1)
            }

            DatePicker("Abholung", selection: $pickup, in: Date.now..., displayedComponents: .hourAndMinute)

            Text("\(cups) × \(size.rawValue) mit \(milk.rawValue)")
                .font(.headline)
        }
        .padding()
    }
}

Erwartetes Ergebnis

Oben ein segmentierter Picker für die Größe, darunter ein Menü-Knopf für die Milch, ein Stepper, ein Schieberegler, eine Uhrzeitauswahl und eine Zusammenfassung, die sich bei jeder Änderung aktualisiert.

Screenshot folgt: Kaffeebestellung mit allen Auswahl-Elementen

Typische Fehler

Auswahl ändert sich nicht

Fast immer passen die Typen von Tag und Auswahl nicht zusammen. Häufiger Fall: Die Auswahl ist ein Optional CoffeeSize?, die Tags sind aber CoffeeSize. Dann braucht jedes Tag ebenfalls ein Optional: .tag(Optional(size)). Einfacher: Mach die Auswahl nicht optional.

Slider mit Int

Fehlermeldung

Initializer 'init(value:in:step:...)' requires that 'Int' conform to 'BinaryFloatingPoint'

Der genaue Wortlaut kann je nach Xcode-Version abweichen.

Slider braucht einen Fließkommawert. Verwende Double.

Datum wird nicht angezeigt wie erwartet

DatePicker und Text(date, ...) verwenden die Zeitzone und das Format des Geräts. In der Preview entspricht das deinem Mac. Willst du ein anderes Format testen, ändere die Umgebung (Lektion 2.5, 🔴-FAQ).

Picker ohne Beschriftung

Der Titel im Picker("Größe", ...) wird je nach Stil nicht angezeigt, etwa bei .segmented. Lass ihn trotzdem drin: VoiceOver liest ihn vor. Brauchst du ihn auch optisch, schreib einen eigenen Text darüber.

FAQ

Anfänger

Was bedeutet das überhaupt?

Warum braucht das Enum Identifiable? Damit ForEach jede Option eindeutig erkennen kann. Bei Enums reicht var id: Self { self }: Jeder Fall ist ja schon eindeutig.

Kann ein Picker Bilder statt Text zeigen? Ja, jede Option kann eine beliebige View sein, etwa ein Label mit Symbol. Wie sie dargestellt wird, hängt vom Stil ab. .segmented zeigt zum Beispiel nur Text oder nur ein Bild pro Segment.

Wie wähle ich mehrere Optionen gleichzeitig? Ein Picker erlaubt immer genau eine Auswahl. Für Mehrfachauswahl nimmst du mehrere Toggles oder eine Liste mit Auswahl (Lektion 2.16).

Fortgeschritten

Warum verhält es sich so?

Wie zeige ich ein Datum später formatiert an? Mit Text(date, format: .dateTime.weekday(.wide).day().month()) oder Text(date, style: .date) aus Lektion 2.5. Für Rechnungen mit Daten, etwa „in drei Tagen“, gibt es Calendar.current.date(byAdding: .day, value: 3, to: date).

Kann ich einen eigenen Wertebereich für einen Stepper mit Text anzeigen? Ja, mit der ausführlichen Form Stepper { Text(...) } onIncrement: { ... } onDecrement: { ... } steuerst du selbst, was bei jedem Schritt passiert. Damit lassen sich auch nicht-numerische Werte durchschalten.

Profi

Was passiert unter der Haube?

Wie findet ein Picker heraus, welche Option ausgewählt ist? Der Picker vergleicht den Wert aus selection mit den Tags seiner Kinder. Dazu muss der Typ Hashable sein. Bei ForEach über Identifiable-Elemente verwendet SwiftUI die id als implizites Tag, wenn deren Typ zur Auswahl passt. Deshalb funktioniert var id: Self { self } bei Enums so elegant.

Warum ist Date keine reine Kalenderangabe? Ein Date ist ein absoluter Zeitpunkt, intern Sekunden seit dem 1. Jänner 2001. Welcher Tag und welche Uhrzeit das sind, hängt von Zeitzone und Kalender ab. Deshalb rechnet man mit Tagen und Monaten immer über Calendar, nie mit festen Sekundenwerten. Sonst gibt es Überraschungen bei der Sommerzeit.

Übungen

Anfänger

Übung 1: Ansichtsumschalter

Baue einen segmentierten Picker mit den Optionen „Tag“, „Woche“ und „Monat“ auf Basis eines Enums. Darunter steht ein Text, der die gewählte Ansicht nennt.

Lösung Übung 1Erst selbst probieren!
Swift
enum TimeRange: String, CaseIterable, Identifiable {
    case day = "Tag"
    case week = "Woche"
    case month = "Monat"

    var id: Self { self }
}

struct RangeView: View {
    @State private var range = TimeRange.week

    var body: some View {
        VStack(spacing: 20) {
            Picker("Zeitraum", selection: $range) {
                ForEach(TimeRange.allCases) { range in
                    Text(range.rawValue)
                }
            }
            .pickerStyle(.segmented)

            Text("Ansicht: \(range.rawValue)")
        }
        .padding()
    }
}
Fortgeschritten

Übung 2: BMI-Rechner

Baue einen BMI-Rechner mit einem Slider für die Größe (140–210 cm, Schritte von 1) und einem Stepper für das Gewicht (40–200 kg). Zeig den BMI mit einer Nachkommastelle an: Gewicht / (Größe in Metern)².

Tipp: .number.precision(.fractionLength(1)) formatiert auf eine Nachkommastelle.

Lösung Übung 2Erst selbst probieren!
Swift
struct BMIView: View {
    @State private var height = 175.0
    @State private var weight = 70

    var bmi: Double {
        let meters = height / 100
        return Double(weight) / (meters * meters)
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            Text("Größe: \(Int(height)) cm")
            Slider(value: $height, in: 140...210, step: 1)

            Stepper("Gewicht: \(weight) kg", value: $weight, in: 40...200)

            Text("BMI: \(bmi, format: .number.precision(.fractionLength(1)))")
                .font(.largeTitle)
                .bold()
        }
        .padding()
    }
}
Profi

Übung 3: Reiseplaner

Baue einen Reiseplaner mit zwei DatePickern für Abreise und Rückkehr. Die Abreise darf nicht in der Vergangenheit liegen, die Rückkehr nicht vor der Abreise. Zeig darunter an, wie viele Nächte die Reise dauert.

Tipp: Calendar.current.dateComponents([.day], from: start, to: end).day ?? 0 liefert die Anzahl der Tage zwischen zwei Daten.

Zusammenfassung

  • Picker wählt einen Wert aus mehreren. Am besten mit einem Enum, das CaseIterable und Identifiable erfüllt.
  • Stile: .menu, .segmented, .wheel, .inline, .navigationLink.
  • Slider für stufenlose Werte mit in: und step:, Stepper für genaue, kleine Zahlen.
  • DatePicker mit displayedComponents und Bereichen wie Date.now....
  • ColorPicker für Farben.
  • Tags und Auswahl müssen denselben Typ haben.

Spickzettel

Swift
enum Size: String, CaseIterable, Identifiable {
    case s = "Klein", m = "Mittel"
    var id: Self { self }
}

Picker("Größe", selection: $size) {
    ForEach(Size.allCases) { Text($0.rawValue) }
}
.pickerStyle(.segmented)

Slider(value: $volume, in: 0...10, step: 1)
Stepper("Anzahl: \(n)", value: $n, in: 1...10)
DatePicker("Termin", selection: $date, in: Date.now..., displayedComponents: .date)
ColorPicker("Farbe", selection: $color)

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.