По данным ВОЗ, около 15% людей на планете живут с той или иной формой инвалидности. Если ваше приложение не поддерживает VoiceOver, крупный шрифт или достаточный контраст — вы отрезаете часть аудитории. Причём не только пользователей с инвалидностью: пожилые люди, те, кто смотрит в телефон на солнце, и просто люди с усталыми глазами — все они выигрывают от нормального accessibility.
Apple всё строже смотрит на это при ревью в App Store. Приложения для госструктур и образования обязаны соответствовать WCAG 2.1. Хорошая новость: UIKit и SwiftUI дают инструменты из коробки. Плохая: по умолчанию они работают, только если думать об accessibility с самого начала.
VoiceOver — первый тест
VoiceOver — скринридер, встроенный в iOS. Он озвучивает всё на экране и позволяет управлять телефоном жестами без просмотра дисплея. Включите через Settings → Accessibility → VoiceOver и пройдите основной сценарий своего приложения. Это самый быстрый способ понять реальное состояние дел.
Что чаще всего ломается:
- Иконки без меток. Кнопка с иконкой корзины читается как «image» — VoiceOver не знает, что это удалить.
- Группировка элементов. Карточка с заголовком, описанием и кнопкой читается как три отдельных элемента.
- Кастомные контролы. Всё, что не UIButton и не UILabel, VoiceOver не обрабатывает автоматически.
Как исправить иконку без метки в UIKit:
let deleteButton = UIButton()
deleteButton.setImage(UIImage(systemName: "trash"), for: .normal)
deleteButton.accessibilityLabel = "Удалить элемент"
deleteButton.accessibilityHint = "Удаляет выбранный элемент без возможности восстановления"
В SwiftUI то же самое:
Button(action: deleteItem) {
Image(systemName: "trash")
}
.accessibilityLabel("Удалить элемент")
.accessibilityHint("Удаляет выбранный элемент без возможности восстановления")
accessibilityLabel — что это такое. accessibilityHint — что произойдёт при активации. Label читается сразу, hint — с паузой и только если пользователь не торопится.
Семантика элементов и accessibilityTraits
VoiceOver умеет сообщать тип элемента: «кнопка», «поле ввода», «переключатель». Это accessibilityTraits. Для стандартных элементов UIKit проставляет их автоматически. Проблемы начинаются с кастомных.
Пример: вы сделали карточку через UIView с UITapGestureRecognizer. VoiceOver увидит просто вьюшку, а не кнопку. Пользователь не поймёт, что её можно нажать.
cardView.isAccessibilityElement = true
cardView.accessibilityTraits = .button
cardView.accessibilityLabel = "Открыть профиль Алексея"
Полезные traits:
.button — кнопка, реагирует на tap
.link — ссылка, открывает что-то внешнее
.header — заголовок секции (позволяет быстро переключаться между секциями)
.selected — элемент выбран
.adjustable — значение меняется свайпом вверх/вниз (как слайдер)
.image — декоративная картинка, неинтерактивная
Группировка элементов
Карточка продукта с названием, ценой и кнопкой «В корзину» — по умолчанию VoiceOver читает их как три элемента. Пользователю три свайпа, чтобы понять суть карточки.
В SwiftUI просто:
VStack {
Text("iPhone 15 Pro")
Text("99 990 ₽")
Button("В корзину") { ... }
}
.accessibilityElement(children: .combine)
.combine собирает тексты всех дочерних элементов. Если результат неудобный — используйте .ignore и задайте label вручную на контейнере.
В UIKit:
let element = UIAccessibilityElement(accessibilityContainer: containerView)
element.accessibilityLabel = "iPhone 15 Pro, 99 990 рублей"
element.accessibilityTraits = .button
element.accessibilityHint = "Добавить в корзину"
element.accessibilityFrameInContainerSpace = containerView.bounds
containerView.accessibilityElements = [element]
Dynamic Type — крупный шрифт без поломки интерфейса
Dynamic Type позволяет менять размер шрифта системно. Используют не только слабовидящие — многие просто предпочитают мелкий или крупный текст. Настройка в Settings → Display & Brightness → Text Size.
Главная ошибка — зашить размер цифрой:
// Плохо
label.font = UIFont.systemFont(ofSize: 17)
// Хорошо
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
В SwiftUI Dynamic Type работает автоматически для стандартных стилей:
Text("Привет")
.font(.body) // масштабируется автоматически
Кастомный шрифт:
Text("Привет")
.font(.custom("YourFont-Regular", size: 17, relativeTo: .body))
Проверить легко: в Xcode включите Environment Overrides (кнопка с термометром в дебаг-тулбаре) и двигайте ползунок шрифта. Следите, не обрезается ли текст, не накладываются ли элементы.
Типичные грабли:
- Фиксированная высота ячейки. Используйте
UITableView.automaticDimension.
- Текст в одну строку (
numberOfLines = 1) там, где должно быть несколько.
- Иконки, которые не масштабируются вместе с текстом рядом.
Цвет и контраст
WCAG 2.1 требует минимальный коэффициент контраста 4.5:1 для обычного текста и 3:1 для крупного (от 18pt). Но даже без обязательных требований низкий контраст — это просто неудобно. Серый текст на белом фоне плохо читается на солнце и у людей с возрастными изменениями зрения.
Проверить контраст: Figma (плагин Contrast), WebAIM Contrast Checker, или Xcode → Accessibility Inspector → Audit.
Ещё одна проблема — цвет как единственный носитель информации. Если вы показываете ошибку только красным цветом без текста — дальтоники её не заметят. Добавьте иконку или текстовое пояснение.
Для Dark Mode используйте семантические цвета, а не хардкодные:
// UIKit
label.textColor = .label
view.backgroundColor = .systemBackground
// SwiftUI
Text("Привет")
.foregroundStyle(.primary)
Switch Control и минимальный размер элементов
Switch Control — режим для пользователей с ограниченными моторными возможностями. Они управляют телефоном через внешний переключатель. Приложение сканирует элементы по очереди.
Аpple требует минимум 44×44 pt для интерактивных элементов. Если кнопка визуально маленькая — расширьте зону касания:
// UIKit
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let expandedBounds = bounds.insetBy(dx: -10, dy: -10)
return expandedBounds.contains(point)
}
// SwiftUI
Button("Tap") { ... }
.frame(minWidth: 44, minHeight: 44)
Для Switch Control важен правильный порядок обхода элементов. По умолчанию это порядок в иерархии вью. Если он неочевидный — настройте accessibilityElements на контейнере вручную.
Accessibility Inspector — обязательный инструмент
Xcode → Open Developer Tool → Accessibility Inspector. Показывает accessibility-дерево: какие элементы видит VoiceOver, их метки, traits и фреймы.
Режим Audit автоматически находит: элементы без меток, слишком маленькие кнопки, низкий контраст.
Алгоритм работы:
- Запустите Audit на каждом экране.
- Исправьте все ошибки с «!» — они критичны.
- Посмотрите предупреждения — не всё нужно исправлять, но стоит понимать.
Параллельно тестируйте VoiceOver на реальном устройстве. Симулятор не передаёт жесты так, как они работают в жизни.
Декоративные элементы
Декоративные картинки и разделители должны быть скрыты от VoiceOver — иначе он будет зачитывать лишнее:
// UIKit
decorativeImageView.isAccessibilityElement = false
// SwiftUI
Image("decoration")
.accessibilityHidden(true)
Обратная сторона: не скрывайте ничего, что несёт смысл. Иллюстрация, поясняющая инструкцию, должна иметь accessibilityLabel с описанием.
Локализация accessibility-текстов
Если приложение многоязычное — accessibility labels тоже должны быть локализованы. Частая ошибка: текст кнопки переведён, а accessibilityLabel хардкодом на русском или английском.
// Localizable.strings
"delete_button_label" = "Delete item";
"delete_button_hint" = "Permanently removes the selected item";
button.accessibilityLabel = NSLocalizedString("delete_button_label", comment: "")
button.accessibilityHint = NSLocalizedString("delete_button_hint", comment: "")
Частые ошибки
Добавлять accessibility в конце. Самая дорогая ошибка. Кастомные компоненты без учёта accessibility приходится переписывать или городить обёртки. Думайте об этом при проектировании.
Переусердствовать с метками. Если на кнопке написано «Сохранить» — не добавляйте accessibilityLabel = "Нажмите, чтобы сохранить". VoiceOver прочитает текст кнопки автоматически. Добавляйте label только если он добавляет информацию, которой нет визуально.
Игнорировать accessibilityValue. Для слайдеров и степперов: accessibilityLabel = "Громкость", accessibilityValue = "70%". Без value пользователь слышит название элемента, но не его текущее состояние.
Не тестировать с реальными пользователями. Никакой инструмент не заменит человека, который использует VoiceOver каждый день. Если такой возможности нет — попросите кого-то из команды провести 30 минут с включённым скринридером на основных сценариях. Это открывает глаза даже опытным разработчикам: неудобный порядок элементов, длинные бессмысленные метки, формально рабочий интерфейс с неудобной навигацией.
Accessibility — это не отдельная задача в конце спринта, а часть качества кода наравне с тестами и производительностью. Начните с VoiceOver и Accessibility Inspector, пройдите основные сценарии с включённым скринридером — это час работы, который покажет 80% проблем. Остальное исправляется итерационно.
Если вы планируете мобильный проект и хотите заложить правильную архитектуру с самого начала — в REEXY занимаются разработкой и интеграцией цифровых продуктов, детали на r3xy.ru.