Реализация Semantic Labels для элементов интерфейса мобильного приложения
Semantic Labels — это не просто accessibilityLabel. Это правильно выстроенная система описаний, подсказок, ролей и состояний для каждого элемента интерфейса, которую screen reader озвучивает пользователю. Разница между «кнопка» (VoiceOver читает что-то по умолчанию) и «Добавить в избранное, кнопка, не активировано» — это разница между приложением, которым можно пользоваться, и приложением, которым нельзя.
Из чего состоит полное описание элемента
Label, Hint, Trait, Value
На iOS это четыре отдельных свойства UIAccessibilityElement:
- Label — что это: «Фото профиля Ивана Петрова», «Кнопка отправить»
- Hint — что произойдёт при активации (необязателен если label самодостаточен): «Открывает страницу профиля»
-
Traits — тип и состояние:
.button,.link,.image,.header,.selected,.notEnabled - Value — текущее значение для элементов с состоянием: слайдер «громкость 70%», переключатель «включено»
VoiceOver озвучивает их в порядке: label → value → traits → hint. Пауза между label и value, пауза перед hint.
В SwiftUI: .accessibilityLabel(), .accessibilityHint(), .accessibilityAddTraits(), .accessibilityValue().
На Android аналог: contentDescription (= label + value), AccessibilityNodeInfo.setRoleDescription() (= custom trait), stateDescription (API 30+).
Составные элементы
Карточка товара с изображением, названием, ценой и кнопкой «В корзину»:
Плохо: VoiceOver фокусируется на каждом subview отдельно — 4 шага, чтобы добраться до кнопки. Screen reader читает: «изображение», «Nike Air Max 90», «8 900 рублей», «кнопка».
Правильно: объединить в один элемент с составным label: «Nike Air Max 90, 8 900 рублей» + trait .button + hint «Добавляет в корзину». Плюс отдельный элемент — кнопка «В корзину» — если пользователю нужен быстрый доступ к ней без прочтения всей карточки.
В UIKit: containerView.accessibilityElements = [productAccessibilityElement, addToCartButton]. productAccessibilityElement — кастомный UIAccessibilityElement с нужным accessibilityFrame и label из нескольких полей.
Динамические состояния
Кнопка «Избранное» — иконка сердечка, без текста, меняет состояние. Базовый label «Добавить в избранное». При добавлении — нужно обновить:
- iOS:
accessibilityLabel = "Удалить из избранного", или черезaccessibilityTraits.insert(.selected)+ label «Избранное» (VoiceOver добавит «выбрано») - После изменения:
UIAccessibility.post(notification: .announcement, argument: "Добавлено в избранное")— мгновенное уведомление без перефокусировки
Переключатели (UISwitch, Toggle в SwiftUI, Switch в Compose) — состояние озвучивается автоматически: «Уведомления, включено». Для кастомных toggle — нужно задавать accessibilityValue = isOn ? "включено" : "выключено".
Группы-заголовки
Экран с несколькими секциями: accessibilityTraits = .header у заголовка секции — пользователь VoiceOver может перемещаться между заголовками свайпом с выбором «Headings» в accessibility rotor. Без этого нельзя быстро перейти к нужной секции без линейного обхода всех элементов.
В Compose: Modifier.semantics { heading() } у Text-заголовка.
Типичные ошибки
Иконки кнопок — contentDescription = "ic_heart" (название файла ресурса) вместо «Добавить в избранное». Android Studio предупреждает, но разработчики часто оставляют как есть.
Placeholder в TextField как label: hint = "Введите email" в Android EditText — TalkBack прочитает hint как contentDescription только если contentDescription не задан. При фокусировке hint исчезает и описание пропадает. Нужен явный contentDescription или использование TextInputLayout с hint, который плавает при фокусе.
Кнопки с числовыми бейджами: «3» рядом с иконкой колокольчика — screen reader читает «3, кнопка» без контекста. Нужно: accessibilityLabel = "Уведомления, 3 непрочитанных".
Срок: 1-3 дня в зависимости от количества компонентов. Часто совмещается с VoiceOver/TalkBack аудитом. Стоимость рассчитывается индивидуально.







