Semantik in SwiftUI und Jetpack Compose
VoiceOver und TalkBack lesen nicht den Bildschirm, sondern eine unsichtbare Beschreibung dahinter: den Accessibility-Baum. In den deklarativen UI-Frameworks SwiftUI und Jetpack Compose entscheidest du über eine Handvoll Modifier, was darin steht: Name, Rolle, Wert und mögliche Aktionen jedes Elements. Diese Seite zeigt die wichtigsten Werkzeuge beider Frameworks und wann du Elemente zusammenfassen, trennen oder mit eigenen Aktionen ausstatten solltest.
Was Semantik bedeutet
Eine Hilfstechnologie wie VoiceOver oder TalkBack sieht keine Pixel. Sie liest eine strukturierte Beschreibung deiner Oberfläche, den Accessibility-Baum(auch Semantik-Baum genannt). Jeder Knoten darin trägt die Informationen, die der Screenreader vorliest und bedienbar macht: einen Namen(Label), eine Rolle(Taste, Überschrift, Schalter), einen Wert(etwa „eingeschaltet") und eine Liste von Aktionen.
In SwiftUI und Jetpack Compose beschreibst du die Oberfläche
deklarativ, und beide Frameworks erzeugen diesen Baum automatisch mit.
Standard-Bausteine sind schon sinnvoll vorbelegt: Ein Button
bekommt die Rolle „Taste", ein Text
liefert seinen Inhalt als Namen. Semantik
ist die Arbeit, die
darüber hinausgeht: dort nachhelfen, wo das Framework nicht raten
kann, etwa bei Icon-Buttons, eigenen Controls oder zusammengesetzten
Karten.
SwiftUI: die wichtigsten Modifier
In SwiftUI hängst du Semantik als Modifier an eine View. Die vier, mit
denen du fast alles abdeckst: accessibilityLabel
setzt den
Namen, accessibilityValue
den aktuellen Wert, accessibilityHint
einen kurzen Hinweis auf die Wirkung,
und accessibilityAddTraits
ergänzt Rollen und Zustände wie .isButton
, .isHeader
oder .isSelected
.
SwiftUI: Name, Rolle und Wert setzen
// Icon-Button beschriften (sonst hört man nur „Taste") Button(action: send) { Image(systemName: "paperplane") } .accessibilityLabel("Nachricht senden") // Eine Textzeile als Überschrift markieren Text("Posteingang") .accessibilityAddTraits(.isHeader) // Wert eines eigenen Reglers ansagen StarRating(value: rating) .accessibilityLabel("Bewertung") .accessibilityValue("\(rating) von 5 Sternen")
Zwei Hinweise aus der Praxis: Formuliere das Label ohne die
Rolle(„Senden", nicht „Senden-Taste"), die Rolle ergänzt
VoiceOver selbst aus dem Trait. Und blende rein dekorative Elemente mit accessibilityHidden(true)
aus, damit der Fokus nicht an
bedeutungslosen Bildern hängen bleibt.
Jetpack Compose: das Semantics-System
Compose bündelt dieselben Informationen im Semantics-System. Jeder Composable kann über den
Modifier Modifier.semantics { … }
Eigenschaften setzen.
Die wichtigsten: contentDescription
für den Namen, role
für die Rolle (etwa Role.Button
), stateDescription
für den Zustand und heading()
,
um einen Knoten als Überschrift zu markieren. Bei einem Icon
oder Image
setzt du die contentDescription
direkt als Parameter.
Jetpack Compose: contentDescription, Rolle und Überschrift
// Icon-Button: contentDescription beschreibt die Funktion IconButton(onClick = ::send) { Icon( Icons.Filled.Send, contentDescription = "Nachricht senden" ) } // Eine Textzeile als Überschrift markieren Text( text = "Posteingang", modifier = Modifier.semantics { heading() } ) // Dekoratives Bild aus dem Baum nehmen Image(painter = bg, contentDescription = null)
Ein wichtiges Detail: Bei einem dekorativen Bild setzt du contentDescription = null
(nicht den leeren String). Damit
sagst du Compose ausdrücklich, dass dieses Element für
Hilfstechnologien bedeutungslos ist, und es verschwindet aus dem Baum.
| Aufgabe | SwiftUI | Jetpack Compose |
|---|---|---|
| Namen setzen | accessibilityLabel
|
contentDescription
|
| Rolle ergänzen | accessibilityAddTraits
|
role = Role.…
|
| Überschrift | .isHeader
-Trait |
heading()
|
| Zustand / Wert | accessibilityValue
|
stateDescription
|
| Element ausblenden | accessibilityHidden(true)
|
contentDescription = null
bzw. clearAndSetSemantics
|
Zusammenfassen und Trennen
Eine Karte aus Bild, Titel und Untertitel besteht aus mehreren Elementen. Linear durch jedes einzelne zu wischen ist mühsam. Oft ist es besser, sie zu einem Element zusammenzufassen, das VoiceOver oder TalkBack in einem Rutsch vorliest.
In SwiftUI macht das accessibilityElement(children:)
. Mit .combine
werden die Beschriftungen der Kinder zu einem
Element verschmolzen, mit .contain
bleiben sie als Gruppe
mit eigenen Kindern erhalten, mit .ignore
werden sie
verworfen, damit du das Element komplett selbst beschreibst. In Compose
verschmilzt Modifier.semantics(mergeDescendants = true)
die Kinder; Modifier.clearAndSetSemantics { … }
wirft die
Semantik der Kinder weg und ersetzt sie durch deine eigene.
.combine
(SwiftUI) beziehungsweise mergeDescendants
(Compose) wird sie zu einem Element,
das in einem Stopp vorgelesen wird.Eigene Aktionen statt versteckter Gesten
Manche Funktionen sind visuell an Gesten gebunden, etwa „nach links wischen zum Löschen" in einer Liste. Für Screenreader-Nutzer:innen ist so eine Geste unsichtbar. Die Lösung sind benannte eigene Aktionen: Du hängst dem Element eine Aktion mit klarem Namen an, die VoiceOver und TalkBack im Aktionen-Menü anbieten.
Eigene Aktion: SwiftUI und Compose
// SwiftUI: benannte Aktion am Listeneintrag row.accessibilityAction(named: "Löschen") { delete(item) } // Jetpack Compose: customActions in den Semantics Modifier.semantics { customActions = listOf( CustomAccessibilityAction("Löschen") { delete(item); true } ) }
So bleibt die schnelle Wisch-Geste für alle erhalten, und Screenreader-Nutzer:innen bekommen denselben Befehl über einen klar benannten Weg. Das ist fast immer besser, als die Geste nachzubauen.
Typische Fallstricke
Die immer gleichen Muster sorgen dafür, dass eine technisch saubere App für Screenreader trotzdem unbrauchbar wird, und sind fast immer schnell behoben:
| Fallstrick | Folge | Lösung |
|---|---|---|
| Icon-Button ohne Label | Ansage „Taste" ohne Funktion | accessibilityLabel
bzw. contentDescription
|
| Rolle im Namen wiederholt | Doppelte Ansage „Senden-Taste, Taste" | Namen ohne Rolle formulieren, Rolle als Trait/Role setzen |
| Dekoratives Bild im Baum | Fokus bleibt an bedeutungslosen Elementen hängen | accessibilityHidden(true)
bzw. contentDescription = null
|
| Karte als viele Einzelstopps | Mühsame Navigation, Zusammenhang unklar | .combine
bzw. mergeDescendants
|
| Funktion nur per Wisch-Geste | Für Screenreader unerreichbar | benannte eigene Aktion ergänzen |
Testen mit den Bordwerkzeugen
Den Accessibility-Baum kannst du dir direkt ansehen, ohne zu raten. Unter iOS zeigt der Accessibility Inspector aus Xcode pro Element Label, Wert, Traits und Aktionen. Unter Android prüfst du mit dem Layout Inspector den Semantik-Baum und mit Accessibility Scanner typische Fehler. Der ehrlichste Test bleibt aber, die App einmal selbst mit VoiceOver oder TalkBack zu bedienen.
Zwei verbreitete Irrtümer
Was über Semantik oft falsch erzählt wird
„Die Frameworks machen Barrierefreiheit automatisch." Sie machen viel automatisch, das stimmt: Standard-Komponenten sind sinnvoll vorbelegt. Aber genau dort, wo es individuell wird, bei Icon-Buttons, eigenen Controls, zusammengesetzten Karten und Gesten-Funktionen, kann das Framework nicht raten. Diese Lücken füllst nur du mit Semantik.
„Ein Hinweis ( accessibilityHint
) an jedem
Element hilft." Zu viele Hinweise machen die Bedienung
langsam und nervig. Ein Hinweis ist sinnvoll, wenn die Wirkung nicht
offensichtlich ist. Name und Rolle dagegen sind die Pflicht, der
Hinweis ist die Kür, sparsam eingesetzt.
Wie die Screenreader, für die du das alles baust, sich tatsächlich bedienen, zeigen die Artikel zu VoiceOver und TalkBack. Die offiziellen Quellen von Apple und Google sind unten verlinkt.
Stimmt der Accessibility-Baum deiner App?
Wir prüfen deine SwiftUI- und Compose-Oberflächen mit echten Hilfstechnologien, decken fehlende Labels, Rollen und Aktionen auf und schulen dein Team, damit Semantik von Anfang an mitwächst, statt am Ende nachgerüstet zu werden.
Beratung oder Schulung anfragen
