Accessibility Trait — to właściwość elementu iOS, która określa jego rolę i zachowanie dla VoiceOver. Trait informuje czytnik ekranu, jak element powinien być odczytany i jakie gesty są dostępne: czy jest przyciskiem, nagłówkiem, linkiem czy polem wyszukiwania. Według danych Apple UIAccessibilityTraits, 2024, system obsługuje 15+ stałych, które można łączyć za pomocą maski bitowej. Prawidłowo wybrany trait oszczędza do 50% czasu nawigacji użytkownikom VoiceOver.
Najważniejsze
Accessibility Trait — flaga ustawiana na elemencie UIView w celu wskazania jego semantycznej roli dla VoiceOver. Trait jest jednym z trzech komponentów triady accessibility Apple: Label (nazwa), Hint (opis), Trait (rola). iOS używa maski bitowej UIAccessibilityTraits (UInt64), gdzie każdy bit odpowiada określonej roli. VoiceOver odczytuje rolę po Label i Hint: „Przycisk Wyślij. Otworzy formularz” — „Przycisk” został dodany dzięki traitowi UIAccessibilityTraitButton.
Domyślnie UIButton otrzymuje UIAccessibilityTraitButton, UILabel — UIAccessibilityTraitStaticText, UIImageView — UIAccessibilityTraitImage. W przypadku niestandardowych kontrolek programista ma obowiązek ręcznego ustawienia traita. Apple Human Interface Guidelines, 2024, nazywają to „jednym z najważniejszych kroków w zapewnieniu accessibility”.
Bez prawidłowego traita użytkownik nie wie, jaki gest zastosować: pojedyncze dotknięcie (aktywacja przycisku), podwójne dotknięcie (powiększenie) lub gest przesunięcia (przełącznik). Trait decyduje, które gesty VoiceOver aktywuje na elemencie.
UIAccessibilityTraits — to typealias UInt64. Każdy trait to stała, w której ustawiony jest dokładnie jeden bit. Na przykład UIAccessibilityTraitButton = 0x0000000000000001, UIAccessibilityTraitLink = 0x0000000000000002, UIAccessibilityTraitHeader = 0x0000000000000008. Kombinację osiąga się przez bitowe OR: 0x0001 | 0x0008 = 0x0009. VoiceOver analizuje maskę i określa zachowanie.
iOS udostępnia ponad 15 stałych traitów. Omówmy główne, używane w 90% scenariuszy:
| Trait | Stała | Zachowanie VoiceOver |
|---|---|---|
| Button | UIAccessibilityTraitButton | Aktywacja przez podwójne dotknięcie |
| Header | UIAccessibilityTraitHeader | Szybka nawigacja po nagłówkach |
| Link | UIAccessibilityTraitLink | Aktywacja jako link |
| StaticText | UIAccessibilityTraitStaticText | Tylko do odczytu, bez aktywacji |
| SearchField | UIAccessibilityTraitSearchField | Pole wyszukiwania ze specjalnym zachowaniem |
| Image | UIAccessibilityTraitImage | Obraz, bez gestu aktywacji |
| Selected | UIAccessibilityTraitSelected | Stan „wybrany” |
| PlaysSound | UIAccessibilityTraitPlaysSound | Odtwarza dźwięk przy aktywacji |
| KeyboardKey | UIAccessibilityTraitKeyboardKey | Klawisz klawiatury |
| TabBar | UIAccessibilityTraitTabBar | Element paska kart |
Stałe są dostępne w UIKit od iOS 3.0. W iOS 14+ dodano obsługę UIAccessibilityTraits w SwiftUI poprzez modyfikator .accessibilityAddTraits().
UIAccessibilityTraitAdjustable — dla regulowanych wartości (suwaki, pokrętła, suwaki głośności). VoiceOver pozwala przesuwać w górę/w dół w celu zmiany wartości z krokiem określonym przez accessibilityIncrement i accessibilityDecrement. UIAccessibilityTraitUpdatesFrequently — dla elementów o często zmieniającej się wartości (timer, wskaźnik ładowania). VoiceOver nie odczytuje wartości przy każdej zmianie, ale robi pauzę. UIAccessibilityTraitAllowsDirectInteraction — dla elementów, z którymi użytkownik może wchodzić w interakcję bezpośrednio (klawiatura, rysowanie), z pominięciem gestów VoiceOver.
Jeden element może mieć kilka traitów jednocześnie — kombinację określa się przez bitowe OR (|). Przykład: przycisk, który jest aktualnie wybrany — Button | Selected. VoiceOver wypowie: „Wybrano. Filtrowane według ceny. Przycisk”.
Ustawianie traitów w kodzie:
filterButton.accessibilityTraits.insert(.button)
filterButton.accessibilityTraits.insert(.selected)
// Albo przez maskę:
filterButton.accessibilityTraits = [.button, .selected]
Dla niestandardowych UIView, gdzie trait nie jest ustawiony domyślnie:
class CustomToggle: UIControl {
override var accessibilityTraits: UIAccessibilityTraits {
get {
if isOn {
return [.button, .selected]
} else {
return .button
}
}
set {}
}
}
Zasada łączenia: nie więcej niż 3-4 traity na element. Nadmierne traity (np. Button + Link + Header) sprawiają, że deklaracja VoiceOver jest zbyt długa i myląca. Według Apple, „każda dodatkowa właściwość zwiększa obciążenie poznawcze użytkownika”.
W SwiftUI traity ustawia się za pomocą modyfikatorów .accessibilityAddTraits() i .accessibilityRemoveTraits(). Przykład: Text("Nagłówek").font(.largeTitle).accessibilityAddTraits(.isHeader). Modyfikator .isHeader dodaje UIAccessibilityTraitHeader. Lista traitów SwiftUI: .isButton, .isHeader, .isLink, .isSelected, .isImage, .isSearchField, .isKeyboardKey, .isStaticText, .isSummaryElement, .isToggle, .playsSound, .startsMediaSession, .updatesFrequently, .allowsDirectInteraction, .causesPageTurn, .isModal, .tabBar.
StaticText zamiast Button — niestandardowa kontrolka, wizualnie wyglądająca jak przycisk, otrzymuje domyślnie trait StaticText. VoiceOver nie oferuje gestu aktywacji, użytkownik nie może „nacisnąć” elementu. Rozwiązanie: jawnie ustaw .button.
Image bez traita — UIImageView z włączoną accessibility otrzymuje trait Image, nawet jeśli w rzeczywistości jest to przycisk do powiększania zdjęcia. Przypisz .button i Label „Powiększ zdjęcie”. Według WWDC 2023, „Deliver an Exceptional Accessibility Experience”, 40% regresji accessibility w nowych wersjach aplikacji wynika właśnie z niezgodności traita.
Header na każdym elemencie — trait Header jest przeznaczony dla strukturalnych nagłówków ekranu. Jeśli uczynisz każdy UILabel nagłówkiem, rotor VoiceOver w trybie „Nagłówki” stanie się bezużyteczny — będzie zatrzymywał się na każdym słowie.
Najczęstsza przyczyna utraty traita — refaktoring: programista zastępuje UIButton na UIControl dla niestandardowego wyglądu. UIButton automatycznie otrzymuje trait Button, UIControl — nie. Po refaktoringu należy jawnie ustawić accessibilityTraits = .button. Dodaj sprawdzenie do code review: „Jeśli zastąpiono UIButton na UIControl — sprawdź trait”.
Dla elementów o zmieniającym się stanie (np. przycisk polubienia) trait powinien zmieniać się dynamicznie. W stanie „nie polubione” — Button, w stanie „polubione” — Button + Selected + Image (jeśli ikona). VoiceOver zmienia komunikat: „Lubię to. Przycisk” vs „Wybrano. Lubię to. Przycisk”. Użyj accessibilityValue do przekazania stanu, jeśli trait Selected jest niewystarczający. Dotyczy przycisków subskrypcji, ulubionych, filtrów i przełączników.
W Androidzie nie ma bezpośredniego odpowiednika traitów. Zamiast maski bitowej używane są:
Dla niestandardowych View w Androidzie trzeba nadpisać onInitializeAccessibilityNodeInfo:
class CustomButton @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
override fun onInitializeAccessibilityNodeInfo(
info: AccessibilityNodeInfo
) {
super.onInitializeAccessibilityNodeInfo(info)
info.className = "android.widget.Button"
info.isClickable = true
}
}
Programiści Flutter powinni używać parametru semanticsRole w widgecie Semantics: button, header, image, link, textField i inne. Dodatkowo dostępne są semanticsLabel i semanticsHint — pełny odpowiednik triady iOS Label + Hint + Trait.
Dla wersji webowych aplikacji mobilnych (PWA, WebView) używany jest atrybut role z WAI-ARIA: role="button", role="heading", role="link". To bezpośredni odpowiednik accessibilityTraits. W aplikacjach hybrydowych sprawdź, czy WebView przekazuje role ARIA do natywnej warstwy accessibility. W tym celu użyj protokołu UIAccessibilityContainerDataTable w iOS lub setAccessibilityDelegate w Androidzie. WebView z włączonym JavaScript może nie przekazywać ról ARIA poprawnie — testuj osobno.
W Androidzie można dodać niestandardowe akcje do AccessibilityNodeInfo: AccessibilityNodeInfo.AccessibilityAction.ACTION_CLICK i ACTION_LONG_CLICK. To odpowiednik traita Button z dodatkowymi gestami. Dla suwaków użyj ACTION_SET_PROGRESS — odpowiednik Adjustable. Dla Spinner i DatePicker — ACTION_SET_SELECTION, ACTION_SET_DATE i ACTION_SET_TIME.
Xcode Accessibility Inspector — główne narzędzie dla iOS: wybierz element i zobacz pole Traits. Pokaże ono listę ustawionych traitów. Rotor VoiceOver w trybie „Elementy” pozwala przejść przez wszystkie kontrolki ekranu.
Zautomatyzowany test w Swift do sprawdzania traita:
func testSubmitButtonTrait() {
let app = XCUIApplication()
app.launch()
let submitButton = app.buttons["Wyślij"]
XCTAssertTrue(submitButton.isEnabled)
// XCUIElement nie zapewnia bezpośredniego dostępu do traitów
// Sprawdzenie przez aktywację gestu
submitButton.tap()
XCTAssertTrue(app.staticTexts["Formularz wysłany"].exists)
}
Ręczne sprawdzanie przez VoiceOver: włącz VoiceOver, przesuń palcem do elementu, dotknij dwukrotnie — element powinien się aktywować, jeśli to Button. Jeśli element nie reaguje na podwójne dotknięcie, trait jest nieprawidłowy. Użyj gestu Rotor do przełączania między trybami („Nagłówki”, „Linki”, „Przyciski”) — każdy tryb pokaże tylko elementy z odpowiednim traitem.
Przed iOS 14 testy jednostkowe nie miały bezpośredniego dostępu do accessibilityTraits. Od iOS 14 właściwość jest dostępna: XCTAssertEqual(customButton.accessibilityTraits, .button). Użyj tego w testach modułowych do sprawdzania niestandardowych kontrolek. Zaleca się testowanie każdej nowej niestandardowej UIView pod kątem poprawności traita, zwłaszcza po refaktoringu lub zmianie klasy nadrzędnej.
Często zadawane pytania
Do 3-4 traitów na element. Większa liczba sprawia, że deklaracja VoiceOver jest zbyt rozwlekła. Używaj kombinacji: Button + Selected, Header + StaticText.
UIAccessibilityTraitButton. iOS automatycznie ustawia go dla wszystkich instancji UIButton. Jeśli dziedziczysz po UIView i imitujesz przycisk, trait trzeba ustawić ręcznie.
Tak, UIAccessibilityTraitAdjustable — dla elementów z regulowaną wartością (suwaki, pokrętła, liczniki). VoiceOver pozwala przesuwać w górę/w dół w celu zmiany wartości i odczytuje bieżący stan.
Użyj modyfikatora .accessibilityAddTraits(): Text("Nagłówek").font(.title).accessibilityAddTraits(.isHeader). Metoda działa na iOS 14+.
VoiceOver przypisze trait None. Element nie otrzyma roli — czytnik ekranu odczyta tylko Label bez wskazania typu. Użytkownik nie dowie się, czy gest aktywacji jest dostępny.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również