Lernziel
Nach dieser Lektion kannst du Apps bauen, die alle bedienen können: Du testest mit VoiceOver und dem Accessibility Inspector, vergibst sinnvolle Beschriftungen, Werte und Hinweise, fasst Elemente zusammen, bietest eigene Aktionen an, unterstützt Bedienungshilfen wie „Bewegung reduzieren“ und „Fettere Schrift“ und vermeidest die häufigsten Barrieren.
Das Problem: Apps, die manche nicht benutzen können
Schließ die Augen und versuch, eine Ausgabe in deinem Ausgaben-Tracker anzulegen. Oder stell dir vor, deine Hände zittern so stark, dass du winzige Buttons kaum triffst. Oder du siehst Rot und Grün nicht als verschiedene Farben.
Für Millionen Menschen ist das Alltag. Weltweit lebt etwa jeder sechste Mensch mit einer Behinderung, und fast jeder ist irgendwann vorübergehend eingeschränkt: mit einem gebrochenen Arm, in grellem Sonnenlicht, mit einem Baby auf dem Arm.
iOS bietet dafür starke Bedienungshilfen: VoiceOver liest den Bildschirm vor, Schaltersteuerung erlaubt die Bedienung mit einem einzigen Knopf, Sprachsteuerung per Stimme, Dynamic Type vergrößert Text. Aber diese Hilfen können nur so gut sein wie die Informationen, die deine App ihnen gibt.
Die gute Nachricht: SwiftUI macht schon sehr viel automatisch richtig. Mit Standard-Komponenten ist eine App oft zu 80 % zugänglich. Diese Lektion zeigt, wie du die restlichen 20 % schaffst.
Barrierefreiheit hilft allen
Gut lesbare Kontraste helfen in der Sonne. Große Tippflächen helfen beim Gehen. Klare Beschriftungen helfen auch der Sprachsteuerung und Siri. Und eine App, die gut mit VoiceOver funktioniert, ist auch gut mit UI-Tests prüfbar (Lektion 4.12).
VoiceOver ausprobieren
Bevor du etwas verbesserst, erlebe deine App so, wie VoiceOver-Nutzer sie erleben.
Auf dem iPhone: Einstellungen → Bedienungshilfen → VoiceOver. Noch praktischer: Unter Bedienungshilfen → Kurzbefehl VoiceOver auswählen. Dann schaltet ein dreifacher Klick auf die Seitentaste VoiceOver ein und aus.
Die wichtigsten Gesten:
| Geste | Wirkung |
|---|---|
| Einmal tippen | Element auswählen und vorlesen |
| Doppelt tippen (irgendwo) | ausgewähltes Element aktivieren |
| Nach rechts / links wischen | nächstes / vorheriges Element |
| Mit zwei Fingern nach oben wischen | alles ab oben vorlesen |
| Mit drei Fingern wischen | scrollen |
| Nach oben / unten wischen | eigene Aktionen oder Werte ändern |
Geh einmal durch deine App, nur mit Wischen nach rechts. Hörst du bei jedem Element, was es ist und was es tut?
Der Accessibility Inspector
Auf dem Mac gibt es ein Werkzeug, das dir zeigt, was VoiceOver „sieht“: Xcode → Open Developer Tool → Accessibility Inspector.
- Wähle oben links deinen Simulator als Ziel.
- Klick auf das Fadenkreuz und dann auf ein Element im Simulator.
- Der Inspector zeigt Label, Value, Traits und weitere Eigenschaften.
Unter dem Reiter Audit (Symbol mit Warndreieck) findet ein Klick auf Run Audit automatisch typische Probleme: fehlende Beschriftungen, zu kleine Tippflächen, zu geringer Kontrast, Text, der bei großer Schrift abgeschnitten wird.
Auch in UI-Tests kannst du das automatisch prüfen lassen:
@MainActor
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
try app.performAccessibilityAudit()
}Die vier Grundinformationen
Jedes Element, das VoiceOver vorliest, besteht aus bis zu vier Teilen:
| Eigenschaft | Frage | Beispiel |
|---|---|---|
| Label | Was ist das? | „Favorit“ |
| Value | Welchen Wert hat es? | „ein“ / „3 von 5 Sternen“ |
| Trait | Welche Art von Element? | „Taste“, „Überschrift“, „ausgewählt“ |
| Hint | Was passiert beim Aktivieren? | „Fügt das Rezept zu deinen Favoriten hinzu.“ |
VoiceOver liest daraus zum Beispiel: „Favorit, ein, Taste. Fügt das Rezept zu deinen Favoriten hinzu.“
Labels: Was ist das?
Bei Text, Button("Sichern"), Toggle("WLAN", ...) und Label("Teilen", systemImage: ...) ist das Label automatisch der Text. Probleme machen Elemente ohne Text:
Button {
toggleFavorite()
} label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
}VoiceOver liest hier etwas wie „Herz, Taste“, abgeleitet aus dem Symbolnamen. Das ist besser als nichts, sagt aber nicht, wofür der Button da ist. Besser:
Button {
toggleFavorite()
} label: {
Image(systemName: isFavorite ? "heart.fill" : "heart")
}
.accessibilityLabel(isFavorite ? "Aus Favoriten entfernen" : "Zu Favoriten hinzufügen")Noch einfacher ist ein Label mit Text, das nur das Symbol zeigt:
Button("Zu Favoriten hinzufügen", systemImage: "heart") { toggleFavorite() }
.labelStyle(.iconOnly)Der Text ist unsichtbar, aber VoiceOver liest ihn vor (Lektion 3.19).
Gute Labels
- Kurz: „Löschen“ statt „Tippe hier, um diesen Eintrag zu löschen“.
- Ohne Elementtyp: nicht „Löschen-Button“, denn „Taste“ sagt VoiceOver schon über den Trait.
- Mit Großbuchstaben am Anfang, ohne Punkt am Ende. VoiceOver betont dann richtig.
Bilder
Für Bilder gibt es zwei Fälle:
Image("receipt-photo")
.accessibilityLabel("Kassenbon vom Supermarkt, 64,20 Euro") // informativ
Image("decorative-wave")
.accessibilityHidden(true) // nur Dekoration
Image(decorative: "decorative-wave") // KurzformDekorative Bilder auszublenden ist genauso wichtig wie informative zu beschreiben. Sonst muss ein VoiceOver-Nutzer an jedem Schmuckelement vorbeiwischen.
Werte
Ein eigener Regler, eine Bewertung mit Sternen oder ein Fortschrittsring haben einen Wert:
StarRating(rating: rating)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Bewertung")
.accessibilityValue("\(rating) von 5 Sternen")Das liest sich als „Bewertung, 3 von 5 Sternen“. Die einzelnen Stern-Bilder werden durch .ignore ausgeblendet. Dazu gleich mehr.
Traits
Traits sagen, welche Art von Element etwas ist:
Text("Ausgaben")
.font(.title2.bold())
.accessibilityAddTraits(.isHeader).isHeader ist besonders wichtig: VoiceOver-Nutzer können mit dem Rotor (zwei Finger drehen) zu „Überschriften“ wechseln und dann von Überschrift zu Überschrift springen, wie Sehende den Bildschirm überfliegen. Abschnittstitel in List und Form sind automatisch Überschriften, eigene Titel nicht.
Weitere nützliche Traits: .isButton für antippbare Views, die keine Buttons sind, .isSelected für die aktuelle Auswahl (Projekt E, Teil 4), .isLink, .updatesFrequently für Werte wie eine laufende Stoppuhr.
onTapGesture ist kein Button
Eine View mit .onTapGesture sieht für VoiceOver nicht wie etwas Antippbares aus. Verwende für Aktionen einen echten Button (Lektion 3.27). Wenn es wirklich nicht anders geht, ergänze .accessibilityAddTraits(.isButton).
Hints
Ein Hint erklärt, was beim Aktivieren passiert, wenn es nicht offensichtlich ist:
.accessibilityHint("Öffnet die Rezeptdetails.")Hints werden nach einer kurzen Pause vorgelesen, und viele Nutzer haben sie abgeschaltet. Verwende sie sparsam und nur, wenn das Label allein nicht reicht.
Elemente zusammenfassen
Eine Zeile im Ausgaben-Tracker besteht aus Symbol, Titel, Kategorie und Betrag. VoiceOver bleibt standardmäßig an jedem Teil einzeln stehen: „Einkaufswagen, Bild. Wocheneinkauf. Lebensmittel. 64,20 Euro.“ Vier Wischgesten für eine Zeile.
Besser ist ein Element pro Zeile:
HStack {
Image(systemName: icon)
VStack(alignment: .leading) {
Text(title)
Text(category)
}
Spacer()
Text(amount, format: .currency(code: "EUR"))
}
.accessibilityElement(children: .combine).accessibilityElement(children: .combine) fasst alle Kinder zu einem Element zusammen und verbindet ihre Labels: „Wocheneinkauf, Lebensmittel, 64,20 Euro.“
Die drei Varianten:
.combine: Kinder zusammenfassen, ihre Labels werden aneinandergehängt. Meist die richtige Wahl für Zeilen und Karten..ignore: Kinder ausblenden, du setzt Label und Wert selbst. Für eigene Komponenten wie die Sternbewertung..contain: Kinder bleiben einzeln, werden aber als Gruppe behandelt. Für Bereiche wie eine Werkzeugleiste.
In einer List fasst SwiftUI Zeilen oft schon automatisch zusammen. Prüfe trotzdem mit VoiceOver, was wirklich vorgelesen wird.
Gezielt formulieren
Statt der automatischen Verkettung kannst du das Label auch selbst formulieren, um es natürlicher klingen zu lassen:
.accessibilityElement(children: .ignore)
.accessibilityLabel("\(title), \(amount.formatted(.currency(code: "EUR"))), \(category)")Eigene Aktionen
Wischaktionen und Kontextmenüs sind für VoiceOver-Nutzer schwer zu finden. Mit eigenen Aktionen stehen sie direkt am Element zur Verfügung: Der Nutzer wischt nach oben oder unten und hört „Löschen“, „Bearbeiten“, „Duplizieren“.
ExpenseRow(expense: expense)
.accessibilityAction(named: "Bearbeiten") { editing = expense }
.accessibilityAction(named: "Duplizieren") { duplicate(expense) }Für die Wisch-Karte aus Lektion 3.27 sind Aktionen die barrierefreie Alternative zum Wischen:
SwipeCard(card: card)
.accessibilityAction(named: "Annehmen") { accept(card) }
.accessibilityAction(named: "Ablehnen") { reject(card) }Für Werte, die man erhöhen und verringern kann, gibt es accessibilityAdjustableAction:
StarRating(rating: rating)
.accessibilityElement(children: .ignore)
.accessibilityLabel("Bewertung")
.accessibilityValue("\(rating) von 5 Sternen")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: rating = min(rating + 1, 5)
case .decrement: rating = max(rating - 1, 1)
@unknown default: break
}
}Jetzt kann ein VoiceOver-Nutzer die Bewertung mit Wischen nach oben und unten ändern, genau wie bei einem Slider.
Auf Einstellungen reagieren
Viele Bedienungshilfen kannst du über die Environment abfragen:
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@Environment(\.accessibilityDifferentiateWithoutColor) private var withoutColor
@Environment(\.legibilityWeight) private var legibilityWeight
@Environment(\.colorSchemeContrast) private var contrastBewegung reduzieren
.scaleEffect(isActive ? 1.2 : 1)
.animation(reduceMotion ? nil : .bouncy, value: isActive)Große Bewegungen, Parallax, wogende Shader und Endlosanimationen können Menschen mit Gleichgewichtsstörungen Übelkeit verursachen. Ersetze sie bei reduceMotion durch einfaches Überblenden oder lass sie weg.
Ohne Farbe unterscheiden
Ein grüner Haken für „im Budget“ und ein roter für „über Budget“ sind für viele Menschen mit Farbsehschwäche gleich. Unterscheide immer zusätzlich durch Form, Symbol oder Text:
Image(systemName: isOverBudget ? "exclamationmark.triangle.fill" : "checkmark.circle.fill")
.foregroundStyle(isOverBudget ? Color.red : Color.green)Rotes Dreieck gegen grünen Kreis: Die Form trägt die Information auch ohne Farbe.
Dynamic Type und Kontrast
Beides kennst du aus Lektion 3.30: Textstile statt fester Größen, @ScaledMetric, Layout bei großer Schrift umstellen. Und Farben mit ausreichendem Kontrast, mindestens 4,5 : 1 für normalen Text.
Tippflächen
Apple empfiehlt Tippflächen von mindestens 44 × 44 Punkten. Ein kleines Symbol ist schnell nur 20 Punkte groß:
Button {
remove()
} label: {
Image(systemName: "xmark")
.font(.caption)
.frame(width: 44, height: 44) // ✅ Tippfläche vergrößern
.contentShape(.rect)
}
.accessibilityLabel("Entfernen")Das Symbol bleibt klein, aber die antippbare Fläche ist groß genug.
Typische Fehler
Buttons ohne Text
Symbole ohne Label. VoiceOver liest den Symbolnamen oder gar nichts Sinnvolles. Füge .accessibilityLabel hinzu oder verwende Label mit .iconOnly.
Jedes Teil einzeln
Zeilen und Karten zerfallen in viele einzelne Elemente. Fasse sie mit .accessibilityElement(children: .combine) zusammen.
Information nur über Farbe
Rot und Grün ohne zusätzliches Symbol oder Text. Ergänze eine Form oder Beschriftung.
Gesten ohne Alternative
Wischen, Ziehen oder langes Drücken als einziger Weg zu einer Funktion. Biete accessibilityAction oder einen sichtbaren Button an.
FAQ
Anfänger
Was bedeutet das überhaupt?Wird mein Label übersetzt?
Ja, wenn du einen Text-Literal verwendest wie .accessibilityLabel("Löschen"). Er landet wie alle anderen Texte im String Catalog (Lektion 4.17).
Muss ich wirklich jedes Bild beschriften? Jedes informative Bild, ja. Dekorative Bilder blendest du aus. Im Zweifel: Würde ein sehender Nutzer etwas verpassen, wenn das Bild fehlte? Dann braucht es eine Beschreibung.
Fortgeschritten
Warum verhält es sich so?Wie steuere ich die Reihenfolge, in der VoiceOver Elemente vorliest?
Normalerweise folgt sie der Leserichtung, von oben links nach unten rechts. Mit .accessibilitySortPriority(…) innerhalb eines Containers mit .accessibilityElement(children: .contain) kannst du sie ändern. Höhere Werte kommen zuerst.
Was ist der Rotor, und kann ich ihn erweitern?
Der Rotor ist ein Drehmenü von VoiceOver, mit dem man zwischen Navigationsarten wechselt: Überschriften, Links, Zeichen. Mit .accessibilityRotor("Über Budget") { ... } kannst du eigene Einträge hinzufügen, etwa um in einer langen Liste direkt von einer Ausgabe über dem Budget zur nächsten zu springen.
Profi
Was passiert unter der Haube?Wie mache ich ein Diagramm aus Canvas oder Shapes zugänglich?
Mit .accessibilityChildren { ... } gibst du unsichtbare Ersatz-Views an, etwa ein ForEach mit einem Rectangle pro Datenpunkt und passenden Labels und Werten. VoiceOver lässt den Nutzer dann von Punkt zu Punkt wischen. Zusätzlich gibt es Audiografiken: Mit .accessibilityChartDescriptor(_:) beschreibst du die Datenreihen, und VoiceOver kann den Verlauf als Tonhöhe abspielen. Swift Charts macht beides automatisch.
Wie gehe ich mit Elementen um, die sich ständig ändern?
Bei Werten, die sich laufend ändern, etwa einem Timer, setz .accessibilityAddTraits(.updatesFrequently), damit VoiceOver nicht bei jeder Änderung neu vorliest. Wichtige Ereignisse, die sonst unbemerkt blieben, etwa „Speichern erfolgreich“, kündigst du mit AccessibilityNotification.Announcement("Gespeichert").post() an.
Übungen
Übung 1: Der VoiceOver-Rundgang
Aktiviere VoiceOver auf deinem iPhone (oder nutze den Accessibility Inspector) und geh durch eine deiner Apps, nur mit Wischen. Notiere jede Stelle, an der du nicht verstehst, was ein Element ist oder tut. Führe zusätzlich einen Audit im Accessibility Inspector aus.
Übung 2: Ausgaben-Tracker zugänglich machen
Verbessere den Ausgaben-Tracker aus Projekt E: Zeilen werden zu einem Element zusammengefasst, Kategorie-Kacheln im Formular bekommen Labels und .isSelected, der schwebende Plus-Button hat ein Label, die Monatssumme ist eine Überschrift, und jede Zeile hat die Aktionen „Bearbeiten“ und „Duplizieren“.
Übung 3: Zugängliche Sternbewertung
Baue eine Sternbewertung aus fünf antippbaren Sternen. Für VoiceOver ist sie ein Element mit Label „Bewertung“, Wert „3 von 5 Sternen“ und accessibilityAdjustableAction. Ist „Ohne Farbe unterscheiden“ aktiv, werden leere Sterne zusätzlich umrandet statt nur grau gefüllt.
Lösung Übung 3Erst selbst probieren!
struct StarRating: View {
@Binding var rating: Int
@Environment(\.accessibilityDifferentiateWithoutColor) private var withoutColor
var body: some View {
HStack(spacing: 4) {
ForEach(1...5, id: \.self) { star in
Button {
rating = star
} label: {
Image(systemName: star <= rating || !withoutColor ? "star.fill" : "star")
.foregroundStyle(star <= rating ? Color.yellow : Color.gray.opacity(0.4))
.font(.title2)
.frame(width: 44, height: 44)
}
.buttonStyle(.plain)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Bewertung")
.accessibilityValue("\(rating) von 5 Sternen")
.accessibilityAdjustableAction { direction in
switch direction {
case .increment: rating = min(rating + 1, 5)
case .decrement: rating = max(rating - 1, 1)
@unknown default: break
}
}
}
}Für sehende Nutzer sind es fünf Buttons mit großen Tippflächen. Für VoiceOver ist es ein einziger, einstellbarer Regler. Bei „Ohne Farbe unterscheiden“ sind leere Sterne nur umrandet, sodass gefüllt und leer auch ohne Farbe erkennbar sind.
Zusammenfassung
- Barrierefreiheit betrifft viele Menschen und hilft allen. SwiftUI erledigt mit Standard-Komponenten schon viel.
- Testen mit VoiceOver (Kurzbefehl: dreimal Seitentaste), dem Accessibility Inspector und
performAccessibilityAudit(). - Die vier Grundinformationen: Label, Value, Trait, Hint.
- Symbole ohne Text brauchen ein Label, dekorative Bilder werden ausgeblendet, Überschriften bekommen
.isHeader. .accessibilityElement(children: .combine)fasst Zeilen und Karten zusammen.accessibilityActionundaccessibilityAdjustableActionals Alternative zu Gesten.- Auf Bewegung reduzieren, Ohne Farbe unterscheiden, Dynamic Type und Kontrast reagieren. Tippflächen mindestens 44 × 44.
Spickzettel
Button("Favorit", systemImage: "heart") { }.labelStyle(.iconOnly)
.accessibilityLabel("Zu Favoriten hinzufügen")
.accessibilityValue("3 von 5 Sternen")
.accessibilityHint("Öffnet die Details.")
.accessibilityAddTraits(.isHeader) // .isButton, .isSelected
.accessibilityHidden(true) // Dekoration
Image(decorative: "wave")
.accessibilityElement(children: .combine) // .ignore, .contain
.accessibilityAction(named: "Löschen") { delete() }
.accessibilityAdjustableAction { dir in ... }
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.accessibilityDifferentiateWithoutColor) private var withoutColor
.frame(minWidth: 44, minHeight: 44).contentShape(.rect)
try app.performAccessibilityAudit() // im UI-Test