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
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: $sizeist 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“, wirdsizeauf"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:
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:
Stringals Raw Value liefert den Anzeigetext.CaseIterableliefert mitallCasesalle Optionen.Identifiablemitvar id: Self { self }macht jeden Fall eindeutig erkennbar, damitForEachdie 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:
Picker("Größe", selection: $size) { ... }
.pickerStyle(.segmented)| Stil | Aussehen | Gut für |
|---|---|---|
.menu | Knopf, der ein Menü öffnet (Standard außerhalb von Formularen) | viele Optionen, wenig Platz |
.segmented | nebeneinanderliegende Segmente | 2 bis 5 kurze Optionen, sofort sichtbar |
.wheel | Drehrad | Zahlen, Zeiten |
.inline | alle Optionen als Liste (in Formularen) | wenige Optionen, die man sehen soll |
.navigationLink | eigene Seite mit Liste (in Formularen mit Navigation) | viele Optionen |
.palette | Symbole nebeneinander, z. B. in Menüs | Farben, 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
@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:
@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...28legt den Bereich fest, mit einem Range aus Lektion 1.4.step: 0.5lä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
@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
@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:
DatePicker("Geburtstag", selection: $birthday, displayedComponents: .date)
DatePicker("Wecker", selection: $alarm, displayedComponents: .hourAndMinute)Einen erlaubten Bereich festlegen:
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:
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
@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:
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.
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
Ü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!
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()
}
}Ü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!
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()
}
}Ü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
Pickerwählt einen Wert aus mehreren. Am besten mit einem Enum, dasCaseIterableundIdentifiableerfüllt.- Stile:
.menu,.segmented,.wheel,.inline,.navigationLink. Sliderfür stufenlose Werte mitin:undstep:,Stepperfür genaue, kleine Zahlen.DatePickermitdisplayedComponentsund Bereichen wieDate.now....ColorPickerfür Farben.- Tags und Auswahl müssen denselben Typ haben.
Spickzettel
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)