Natywne API dostępności na urządzeniach mobilnych w 2026:
UIAccessibility, AccessibilityNode i sieć
iOS i Android udostępniają czytnikowi ekranu platformy w pełni rozbudowane drzewo dostępności — jednak te dwa drzewa nie zgadzają się ze sobą w niczym poza podstawami: etykietą i rolą. Zmapowaliśmy każdy prymityw faktycznie obsługiwany przez VoiceOver i TalkBack w 2026 roku, sposób, w jaki React Native, Flutter i Kotlin Multiplatform próbują je łączyć, oraz miejsce, gdzie dostępność mobilnych widoków WebView cicho się załamuje.
1. iOS UIAccessibility — etykiety, cechy, wskazówki, wartości
Każdy widoczny element na ekranie iOS ma lub może mieć reprezentację w drzewie dostępności. Apple udostępnia tę reprezentację przez nieformalny protokół UIAccessibility, implementowany przez UIView i każdą kontrolkę systemową, oraz przez klasę UIAccessibilityElement — lekką klasę alokowaną dla tych fragmentów interfejsu, które są rysowane, lecz same nie są widokami: znaki na niestandardowym wykresie, glify wewnątrz sceny Core Graphics, regiony wewnątrz CALayer. VoiceOver, Switch Control, Full Keyboard Access i Voice Control — wszystkie korzystają z tego samego protokołu; nauka raz daje dostęp do czterech technologii wspomagających.
Protokół udostępnia cztery prymitywy istotne dla niemal każdego ekranu. Accessibility label to krótka, czytelna dla człowieka nazwa elementu — „Wyślij“, „Zdjęcie profilowe Ashy“, „Wstecz“. Accessibility traits to maska bitowa flag przypominających role — .button, .header, .image, .selected, .adjustable, .staticText, .updatesFrequently — informujące VoiceOver, jak zachować się wobec elementu i które gesty aktywować. Accessibility value to tekstowa reprezentacja bieżącego stanu („Włączone“, „75%“, „Czwartek, 22 maja“). Accessibility hint to dłuższe, opcjonalne wyjaśnienie („Stuknij dwukrotnie, by otworzyć przeglądarkę zdjęć“), które VoiceOver wypowiada po chwili zwłoki, gdy użytkownik nie zareaguje na samą etykietę.
Cztery prymitywy łączą się ze sobą. Przełącznik odczytywany jest jako etykieta + cecha + wartość: „Wi-Fi, przełącznik, Włączone“. Suwak odczytywany jest jako etykieta + cecha + wartość + wskazówka: „Głośność, regulowany, 60 procent, przesuń w górę lub w dół, aby dostosować“. Niestandardowy słupek wykresu to łańcuch obiektów UIAccessibilityElement, z których każdy ma etykietę, wartość i ramkę wewnątrz kontenera. Ten łańcuch jest powierzchnią API — VoiceOver przemierza go liniowo podczas gestu przesuwania w prawo i respektuje kolejność, w jakiej elementy są publikowane przez tablicę accessibilityElements kontenera.
Modyfikatory widoku .accessibilityLabel(), .accessibilityValue(), .accessibilityHint() i .accessibilityAddTraits() w SwiftUI kompilują się do tych samych właściwości UIAccessibility na bazowym UIView. SwiftUI dodaje również .accessibilityElement(children:), które rozwiązuje problem „znaków na wykresie“ w sposób bardziej deklaratywny niż podejście UIKit — jednak kontrakt runtime widziany przez VoiceOver jest identyczny. Warto poznać nazewnictwo UIKit, bo każdy przykład Apple, każda odpowiedź na Stack Overflow i każdy audyt dostępności posługują się właśnie nimi.
2. Android AccessibilityNodeInfo — role, akcje, importantForAccessibility
Android przyjmuje inne podejście. Tam gdzie iOS przyczepia dostępność do płaskiego protokołu na każdym widoku, Android serializuje całe drzewo dostępności jako graf obiektów AccessibilityNodeInfo, z których każdy jest migawką widoku w chwili nadejścia zapytania TalkBack. Framework tworzy migawki leniwie; View publikuje swój węzeł przez nadpisanie metody onInitializeAccessibilityNodeInfo() (lub w Compose — przez ustawienie modyfikatorów semantycznych), a platforma splata relacje rodzic-dziecko w drzewo odzwierciedlające hierarchię widoków.
Prymitywy różnią się od iOS-owych w trzech istotnych aspektach. Po pierwsze, Android udostępnia rolę przez pole className typowane jako ciąg znaków — android.widget.Button, android.widget.CheckBox, android.widget.EditText. TalkBack odczytuje nazwę klasy i decyduje, jak ogłosić element („przycisk“, „pole wyboru“, „pole edycji“). Compose tłumaczy swoje semantyki Role.Button, Role.Checkbox, Role.RadioButton na to samo pole. Rola jest bardziej szczegółowa niż maska bitowa cech iOS, ale też bardziej sztywna — nie ma roli „całkowicie niestandardowej“, chyba że akceptuje się komunikat „widok“.
Po drugie, Android reprezentuje interaktywność jako zbiór akcji przypisanych do węzła: ACTION_CLICK, ACTION_LONG_CLICK, ACTION_SCROLL_FORWARD, ACTION_SET_TEXT, ACTION_FOCUS oraz długa lista niestandardowych akcji, które można zarejestrować za pomocą AccessibilityNodeInfo.AccessibilityAction. TalkBack udostępnia niestandardowe akcje przez „rotator akcji“ — użytkownik przesuwa jednym palcem w górę i słyszy kolejne nazwy akcji. iOS ma analogiczną koncepcję (UIAccessibilityCustomAction), lecz na Androidzie lista akcji jest powierzchnią; na iOS powierzchnią jest słownik gestów.
Po trzecie, Android posiada importantForAccessibility — wyliczenie per-widok (auto, yes, no, noHideDescendants) kontrolujące, czy węzeł w ogóle pojawia się w drzewie. noHideDescendants to najpotężniejsze narzędzie dostępności Androida i jednocześnie najczęściej pomijane — usuwa cały poddrzew z przeglądania przez TalkBack, odpowiednik aria-hidden=“true” w sieci. iOS nie ma dokładnego odpowiednika; najbliższe jest ustawienie accessibilityElements na pustą tablicę na kontenerze, co usuwa jedynie bezpośrednie dzieci kontenera, nie całe poddrzewo.
Android udostępnia ViewCompat.setAccessibilityLiveRegion() z trzema wartościami: none, polite, assertive. Słownictwo odzwierciedla ARIA — prawie. TalkBack honoruje poziomy grzeczności niezawodnie. iOS nie ma nic porównywalnego na poziomie protokołu: aktualizacje ogłasza się przez wywołanie UIAccessibility.post(notification: .announcement, argument: “Zapisano”), imperatywne jednorazowe wywołanie, które nie jest powiązane z żadnym widokiem. Wieloplatformowe pomosty muszą emulować jedno na bazie drugiego, a niedopasowanie impedancji widać w każdym frameworku omówionym w sekcji 3.
3. Wieloplatformowe pomosty — React Native, Flutter, Kotlin Multiplatform
Każdy wieloplatformowy framework mobilny musi wziąć dwa opisane powyżej API i zaprezentować jedną, zunifikowaną po stronie frameworka powierzchnię. Żadnemu nie udaje się to w pełni. Trzy podejścia dominują na rynku w 2026 roku — React Native, Flutter oraz Kotlin Multiplatform z Compose Multiplatform — każde z nich to nieco inny kompromis między wyciekiem szczegółów a poziomem abstrakcji.
accessibilityLabel, accessibilityHint, accessibilityRole, accessibilityState na Pressable i View mapują się prawie 1:1 na UIAccessibility — jednak nazwy ról to słownictwo React Native, nie iOS.accessibilityRole=“button” ustawia className na android.widget.Button.accessibilityLiveRegion działa tylko na Androidzie — na iOS cicho nic nie robi i trzeba ręcznie wywołać AccessibilityInfo.announceForAccessibility().UIAccessibilityElement we widoku Flutter, z cechami mapowanymi z zestawów SemanticsAction i SemanticsFlag.SemanticsFlag.isLiveRegion.Modifier.semantics { } definiuje role i akcje raz; każdy target tłumaczy ten sam blok semantyczny na własne natywne API dostępności.Wzorzec jest we wszystkich trzech przypadkach taki sam: syntetyczne, uformowane przez framework drzewo semantyczne po jednej stronie, dwa uformowane przez platformę drzewa dostępności po drugiej, i translator pośrodku, który dobrze radzi sobie z prostymi przypadkami, a z złożonymi — ze znaczącą utratą wierności. Proste przypadki — przycisk z etykietą, obraz z tekstem alternatywnym, nagłówek — przechodzą przez pomost bez strat. Złożone — niestandardowy gest dwoma palcami, wykres, którego elementy powinny być fokusowaną grupą, live region, który na iOS musi zadziałać bez powiązania z widokiem — ujawniają słownictwo bazowej platformy w wieloplatformowym kodzie lub po prostu nie dają się przetłumaczyć.
„Pierwsze 80 procent dostępności mobilnej jest identyczne we wszystkich frameworkach. Ostatnie 20 procent to miejsce, w którym każdy framework zdradza, w którym natywnym API naprawdę myśli.“
4. Luka WebView — kiedy dostępność mobilnej sieci cicho zawodzi
Zarówno iOS, jak i Android renderują treści webowe przez systemowy WebView — WKWebView na iOS, android.webkit.WebView (lub Chrome Custom Tabs) na Androidzie. W obu przypadkach WebView jest czarną skrzynką z perspektywy aplikacji hosta: aplikacja widzi jeden widok, lecz czytnik ekranu widzi całe drzewo dostępności DOM wewnątrz. Pomost między tymi dwoma drzewami to miejsce, w którym zadziwiająco duża część dostępności mobilnej po cichu zawodzi.
Mechanizm jest pozornie prosty. Gdy fokus czytnika ekranu wchodzi do WebView, platforma odczytuje drzewo dostępności dokumentu bezpośrednio z silnika przeglądarki — WebKit na iOS, Blink na Androida — i przemierza je jako poddrzewo drzewa aplikacji hosta. Role, etykiety i atrybuty ARIA w sieci są tłumaczone na słownictwo platformy w czasie rzeczywistym. Element button bez jawnej roli wewnątrz WebView odczytywany jest jako przycisk na obu platformach; region aria-live=“polite” ogłaszany jest poprawnie na obu; aria-label na łączu pojawia się jako etykieta dostępności łącza. Przez pierwsze kilka lat mobilnej sieci po prostu działało.
Przepaść pojawia się w trzech miejscach. Po pierwsze, niestandardowe gesty zdefiniowane w aplikacji hosta — gest dwoma palcami do zamknięcia, magic-tap do odtwarzania i pauzy — są niewidoczne dla treści WebView; uruchamiają się na złym celu lub nie uruchamiają się wcale, gdy fokus jest wewnątrz dokumentu. Po drugie, obiekty UIAccessibilityElement hosta rysowane nad WebView (pływający przycisk akcji, niestandardowy pasek narzędzi) konkurują z drzewem WebView o kolejność przeglądania, a wynikowy porządek czytania jest niedeterministyczny w różnych wersjach iOS. Po trzecie — i to jest największy pojedynczy błąd w dostępności mobilnej sieci — WebView na iOS nie honoruje poziomów grzeczności aria-live tak jak Safari w karcie: pomost ogłoszeń WKWebView pomija rozróżnienie między polite a assertive, więc każda aktualizacja na żywo traktowana jest jako polite bez względu na znaczniki.
<div role="alert" aria-live="assertive">
Utracono połączenie — ponawiam próbę.
</div>VoiceOver w normalnej karcie Safari przerywa bieżącą wypowiedź i natychmiast odczytuje komunikat. Poziom grzeczności assertive jest honorowany w całym potoku WebKit.
<div role="alert" aria-live="assertive">
Utracono połączenie — ponawiam próbę.
</div>Ten sam znacznik, ten sam silnik przeglądarki — ale pomost dostępności WKWebView do UIKit obniża rangę ogłoszenia do odroczonego komunikatu polite. Użytkownik słyszy je po chwili zwłoki, czasem już po tym, gdy wpisał dane do uszkodzonego formularza.
W przypadku ogłoszeń wewnątrz WebView jedynym niezawodnym wzorcem wieloplatformowym w 2026 roku jest udostępnienie mostu JavaScript do aplikacji hosta — małego handlera postMessage — i kierowanie asertywnych ogłoszeń poza DOM, do hosta, a następnie z powrotem przez UIAccessibility.post(notification: .announcement, …) na iOS lub announceForAccessibility() na Androida. Web aria-live sprawdza się tylko przy naprawdę grzecznych komunikatach, gdzie akceptowalne jest kilkusekundowe opóźnienie.
5. Tabela odwzorowań — co odpowiada czemu
Zmapowaliśmy 28 prymitywów faktycznie obsługiwanych przez VoiceOver i TalkBack w praktyce — sumę powierzchni protokołu iOS UIAccessibility, powierzchni Android AccessibilityNodeInfo oraz najczęściej używanych właściwości wieloplatformowych React Native i Flutter. Poniższa tabela zawiera wyłącznie sporne wiersze: prymitywy, gdzie odwzorowanie jest niekompletne, asymetryczne lub zaskakujące. Wiersze z czystym odwzorowaniem (etykieta, rola przycisku, rola obrazu, nagłówek) zostały pominięte ze względu na długość.
| Możliwość | iOS UIAccessibility | Android AccessibilityNodeInfo | React Native 0.76 | Flutter 3.27 |
|---|---|---|---|---|
| Tekst wskazówki (dłuższe wyjaśnienie) | accessibilityHint | tooltipText (API 28+) | accessibilityHint (tylko iOS) | SemanticsProperties.hint |
| Poziom grzeczności live region | N/D — wyłącznie imperatywny post | setAccessibilityLiveRegion() | accessibilityLiveRegion (tylko Android) | SemanticsFlag.isLiveRegion |
| Ukrycie poddrzewa przed dostępnością | accessibilityElementsHidden (tylko dzieci) | importantForAccessibility=“noHideDescendants” | accessibilityElementsHidden / importantForAccessibility | Widget ExcludeSemantics |
| Niestandardowa akcja (rotor / menu) | UIAccessibilityCustomAction | AccessibilityNodeInfo.AccessibilityAction | accessibilityActions + onAccessibilityAction | SemanticsAction z niestandardową etykietą |
| Semantyka regulowanego / suwaka | UIAccessibilityTraitAdjustable + accessibilityIncrement | RangeInfo + ACTION_SCROLL_FORWARD | accessibilityRole=“adjustable” + handlery | Slider udostępnia SemanticsAction.increase |
| Poziom nagłówka | UIAccessibilityTraitHeader (bez poziomu) | setHeading(true) (bez poziomu) | accessibilityRole=“header” (bez poziomu) | SemanticsProperties.headingLevel (1–6) |
| Stan wybrany / przełączony | UIAccessibilityTraitSelected | setSelected(true) + setCheckable() | accessibilityState={selected, checked} | SemanticsFlag.isSelected |
| Semantyka grupy / kontenera | shouldGroupAccessibilityChildren | setScreenReaderFocusable(true) | accessible={true} na rodzicu | Widget MergeSemantics |
| Jednorazowe ogłoszenie | UIAccessibility.post(.announcement, …) | view.announceForAccessibility() | AccessibilityInfo.announceForAccessibility() | SemanticsService.announce() |
Z tabeli wynikają trzy wzorce. Po pierwsze, asymetria wokół live regionów to największe źródło rozbieżności wieloplatformowych — Android posiada ustawienie grzeczności per-widok, iOS ma jedynie globalny imperatywny post, a każdy framework powyżej jest zmuszony ukrywać tę różnicę. Po drugie, poziomy nagłówków to jedyne miejsce, gdzie Flutter realnie przewyższa obie natywne platformy; prymitywy iOS i Androida wiedzą tylko, że „to jest nagłówek“, nie że „to jest H3 pod H2“. Po trzecie, prymityw „ukryj przed dostępnością“ jest bardziej elastyczny na Androidzie — noHideDescendants ukrywa całe poddrzewo jednym ruchem, podczas gdy iOS wymaga ukrywania dzieci każdego kontenera osobno.
6. Poradnik praktyczny dla mobilnych deweloperów
Poznaj natywne słownictwo przed słownictwem frameworka
Każdy wieloplatformowy pomost — React Native, Flutter, Compose Multiplatform — ma własne nazewnictwo właściwości dostępności, a każda z tych nazw to drobne kłamstwo o tym, co naprawdę robi bazowa platforma. Gdy czytnik ekranu nie ogłasza poprawnie, błąd prawie zawsze tkwi w natywnym API, na które framework przetłumaczył właściwość, a nie w samej właściwości frameworka. Dokumentację UIAccessibility i AccessibilityNodeInfo warto przeczytać przynajmniej raz; dokumentacja frameworka nabiera sensu dopiero potem.
Testuj ogłoszenia na żywo konkretnie na iOS
Asymetria live regionów z sekcji 2 oznacza, że każdy kod zakładający działanie aria-live=“assertive” lub accessibilityLiveRegion=“assertive” będzie po cichu degradował się na iOS. Należy zbudować małe środowisko testowe, które wysyła zarówno grzeczne, jak i asertywne ogłoszenie na obu platformach — z VoiceOver i TalkBack na prawdziwych urządzeniach — zanim wyśle się jakąkolwiek funkcję, której UX zależy od usłyszenia przez użytkownika informacji o zmianie stanu.
Przekierowuj przez pomost poza WebViews wszystko, co asertywne
Degradacja asertywnych ogłoszeń w WKWebView nie jest błędem, który Apple wkrótce naprawi — zachowanie jest identyczne w każdym wydaniu iOS od 14 wzwyż. Jeśli dostarcza się aplikację hybrydową, gdzie użytkownik może napotkać krytyczny błąd wewnątrz WebView, należy przekierować ogłoszenie przez pomost JS do hosta i pozwolić, by host uruchomił natywne ogłoszenie platformy. Samo webowe rozwiązanie nie wystarczy.
Używaj semantyki „scalenia“ lub „grupowania“ z frameworka, nie dziecko-po-dziecku
Zarówno iOS (shouldGroupAccessibilityChildren), Android (setScreenReaderFocusable), jak i Flutter (MergeSemantics) oferują sposób na zwinięcie wizualnego klastra — ikona plus etykieta plus wartość — w jeden element dostępności. Należy z tego korzystać. Domyślne zachowanie „każdy liść jest fokusowanym elementem“ zamienia sześcioelementowy chip nawigacyjny w sześć gestów przesuwania VoiceOver.
Przeprowadzaj audyt z Accessibility Inspector i TalkBack Developer Settings
Obie platformy dostarczają darmowy, oficjalny inspektor drzewa dostępności na żywo — Accessibility Inspector na macOS (połączony z symulatorem lub urządzeniem iOS) oraz nakładkę „Pokaż fokus dostępności“ i „Ustawienia dewelopera“ na Androida. Należy używać ich do odczytu własnego drzewa aplikacji tak, jak widzi je czytnik ekranu; nie należy zakładać, że logi debugowania frameworka pokazują to samo co TalkBack.
Podsumowanie: framework jest na dalszym planie niż platforma
Kuszące jest — a dokumentacja frameworka sprzyja tej pokusie — przekonanie, że wieloplatformowe API dostępności to jednolita abstrakcja nad dwoma równoważnymi natywnymi API. Tabela odwzorowań w sekcji 5 obala tę unifikację. Dwa natywne API zostały zaprojektowane niezależnie, przez dwa różne zespoły, wokół dwóch różnych modeli mentalnych sposobu, w jaki czytnik ekranu powinien przemierzać dokument; różnice są realne, przebijają się przez każdy framework, a wyciek pojawia się w tych częściach doświadczenia użytkownika, które mają największe znaczenie — aktualizacje na żywo, niestandardowe gesty, ukryte poddrzewa, hierarchie nagłówków.
Dobra wiadomość po tym akapicie: podstawy działają. Przycisk z etykietą, obraz z tekstem alternatywnym, nagłówek na początku sekcji — te elementy przechodzą przez każdy framework i ogłaszane są poprawnie na obu platformach. Jeśli dostarcza się wyłącznie te prymitywy, nie trzeba myśleć o UIAccessibility ani AccessibilityNodeInfo; domyślne ustawienia frameworka są uczciwe. Kłopoty zaczynają się, gdy interfejs zaczyna robić coś interesującego — czyli wtedy, gdy dostępność zaczyna mieć największe znaczenie.
Poradnik z sekcji 6 to najkrótsza wersja argumentu, który prowadzi do działającego doświadczenia dla możliwie największej liczby osób z niepełnosprawnościami: myśleć w natywnych prymitywach, testować na prawdziwych urządzeniach obu platform, przekierowywać przez pomost poza WebViews, gdy mamy asertywne zamiary, grupować liście świadomie i korzystać z oficjalnych inspektorów. Wybrany framework pomaga w pierwszych 80 procentach i nie przeszkadza w ostatnich 20. W tych ostatnich 20 procentach żyje użytkownik czytnika ekranu.
„VoiceOver i TalkBack czytają dwa różne dokumenty z tego samego kodu źródłowego. To, czy użytkownik zauważy różnicę, jest miarą tego, jak dobrze rozumie się platformę leżącą pod wybranym frameworkiem.“