Lektion 4.16 · Stufe 4 – Profi

Barrierefreiheit

ca. 15 Minuten Lesezeit

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:

GesteWirkung
Einmal tippenElement auswählen und vorlesen
Doppelt tippen (irgendwo)ausgewähltes Element aktivieren
Nach rechts / links wischennächstes / vorheriges Element
Mit zwei Fingern nach oben wischenalles ab oben vorlesen
Mit drei Fingern wischenscrollen
Nach oben / unten wischeneigene 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.

  1. Wähle oben links deinen Simulator als Ziel.
  2. Klick auf das Fadenkreuz und dann auf ein Element im Simulator.
  3. 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:

Swift
@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:

EigenschaftFrageBeispiel
LabelWas ist das?„Favorit“
ValueWelchen Wert hat es?„ein“ / „3 von 5 Sternen“
TraitWelche Art von Element?„Taste“, „Überschrift“, „ausgewählt“
HintWas 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:

Swift
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:

Swift
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:

Swift
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:

Swift
Image("receipt-photo")
    .accessibilityLabel("Kassenbon vom Supermarkt, 64,20 Euro")   // informativ

Image("decorative-wave")
    .accessibilityHidden(true)                                     // nur Dekoration

Image(decorative: "decorative-wave")                               // Kurzform

Dekorative 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:

Swift
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:

Swift
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:

Swift
.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:

Swift
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:

Swift
.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“.

Swift
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:

Swift
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:

Swift
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:

Swift
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
@Environment(\.accessibilityDifferentiateWithoutColor) private var withoutColor
@Environment(\.legibilityWeight) private var legibilityWeight
@Environment(\.colorSchemeContrast) private var contrast

Bewegung reduzieren

Swift
.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:

Swift
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ß:

Swift
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

Anfänger

Ü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.

Fortgeschritten

Ü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“.

Profi

Ü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!
Swift
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.
  • accessibilityAction und accessibilityAdjustableAction als Alternative zu Gesten.
  • Auf Bewegung reduzieren, Ohne Farbe unterscheiden, Dynamic Type und Kontrast reagieren. Tippflächen mindestens 44 × 44.

Spickzettel

Swift
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

Lektion geschafft?

Markiere sie als erledigt, damit du deinen Fortschritt siehst.