По данным Apple, более 1 миллиарда человек в мире живут с какой-либо формой ограниченных возможностей. Это не абстрактная цифра — это потенциальные пользователи вашего приложения, которых легко потерять, если не думать об accessibility с самого начала.
Accessibility (доступность) в iOS — это не про "добавить поддержку VoiceOver для галочки". Это про то, чтобы приложение работало для всех: для слепых и слабовидящих, для людей с моторными ограничениями, для тех, кто плохо слышит, и для тех, кому просто неудобно держать телефон одной рукой в переполненном метро.
Почему это важно прямо сейчас
Apple активно следит за качеством accessibility в App Store. Приложения с грубыми нарушениями могут получить реджект. Плюс с 2025 года в ЕС действует European Accessibility Act — и если ваш продукт работает на европейском рынке, соответствие стандартам доступности стало юридическим требованием, а не рекомендацией.
С практической точки зрения: хорошо реализованный accessibility улучшает опыт для всех. Dynamic Type делает текст читабельным для пожилых пользователей. Крупные touch targets удобны при езде в транспорте. Нормальный цветовой контраст помогает на ярком солнце.
VoiceOver: как это работает и что нужно знать
VoiceOver — экранный чтец, встроенный в iOS. Он озвучивает интерфейс и позволяет управлять телефоном без зрения. Чтобы проверить своё приложение, включите его через Настройки → Универсальный доступ → VoiceOver (или тройной клик кнопки Home/Side Button).
Как VoiceOver читает интерфейс:
- Он проходит по элементам в порядке, определённом accessibility frame
- Для каждого элемента читает label, value и hint
- Пользователь свайпает вправо/влево для навигации, двойной тап — действие
Основные атрибуты, которые вы контролируете:
// Задать label — что VoiceOver прочитает
button.accessibilityLabel = "Добавить в избранное"
// Hint — дополнительная подсказка о действии
button.accessibilityHint = "Дважды нажмите, чтобы сохранить товар"
// Value — текущее состояние
slider.accessibilityValue = "75 процентов"
// Скрыть декоративный элемент от VoiceOver
decorationImageView.isAccessibilityElement = false
// Объединить несколько элементов в один
cardView.accessibilityElements = nil
cardView.shouldGroupAccessibilityChildren = true
Главная ошибка, которую делают почти все: иконки без label. Кнопка с SF Symbol "heart" без accessibilityLabel будет прочитана как "heart" или вообще ничего. Пользователь VoiceOver не поймёт, что происходит.
Traits: рассказываем о роли элемента
Accessibility traits сообщают VoiceOver, что за элемент перед ним и как он себя ведёт:
// Элемент является кнопкой
view.accessibilityTraits = .button
// Заголовок — VoiceOver объявит его как heading
titleLabel.accessibilityTraits = .header
// Элемент выбран
tabBarItem.accessibilityTraits = [.button, .selected]
// Ссылка
linkLabel.accessibilityTraits = .link
// Изображение
photoView.accessibilityTraits = .image
Traits можно комбинировать через array literal. Если у вас кастомный toggle — добавьте .button и управляйте .selected в зависимости от состояния.
SwiftUI и accessibility
В SwiftUI многое работает из коробки: Text читается автоматически, Button получает корректные traits. Но для кастомных элементов нужна ручная настройка:
struct HeartButton: View {
@State private var isFavorite = false
var body: some View {
Button(action: { isFavorite.toggle() }) {
Image(systemName: isFavorite ? "heart.fill" : "heart")
}
.accessibilityLabel(isFavorite ? "Убрать из избранного" : "Добавить в избранное")
.accessibilityAddTraits(.isButton)
}
}
Для группировки контента используйте .accessibilityElement(children: .combine) — VoiceOver прочитает всё содержимое как единый элемент:
VStack(alignment: .leading) {
Text(product.name)
Text(product.price)
Text(product.rating)
}
.accessibilityElement(children: .combine)
Вместо трёх отдельных чтений пользователь услышит: "Кроссовки Nike, 5 490 рублей, рейтинг 4.5".
Dynamic Type: текст, который адаптируется
Dynamic Type — система масштабирования текста. Пользователи выбирают предпочтительный размер в Настройках, и приложения должны уважать этот выбор.
Правила простые:
- Используйте системные text styles вместо хардкода размеров шрифта
- Никогда не фиксируйте высоту строк и высоту ячеек
- Тестируйте на размере Accessibility Extra Extra Extra Large
// Правильно — адаптируется под настройки пользователя
label.font = UIFont.preferredFont(forTextStyle: .body)
label.adjustsFontForContentSizeCategory = true
// Неправильно — игнорирует пользовательские настройки
label.font = UIFont.systemFont(ofSize: 16)
В SwiftUI это ещё проще:
Text("Привет")
.font(.body) // автоматически адаптируется
Частая ошибка: фиксированная высота контейнера. Если у вас NSLayoutConstraint с константой, текст при большом шрифте обрежется. Используйте >= для высоты или вообще не ограничивайте её снизу.
Цветовой контраст
WCAG 2.1 требует соотношение контраста минимум 4.5:1 для обычного текста и 3:1 для крупного (от 18pt или 14pt жирного). Проверить можно в Accessibility Inspector (идёт в Xcode) или онлайн-инструментами.
Практически: светло-серый текст на белом фоне — типичная проблема. Дизайнеры любят #999999 на #FFFFFF — это соотношение около 2.85:1, что не проходит по стандарту.
iOS поддерживает Increase Contrast — режим повышенного контраста. Вы можете реагировать на него:
if UIAccessibility.isDarkerSystemColorsEnabled {
label.textColor = .black
} else {
label.textColor = UIColor(named: "PrimaryText")
}
Лучший подход — настроить цвета в Assets с вариантами для High Contrast. Xcode поддерживает это нативно: в Color Set есть отдельные слоты для Normal и High Contrast.
Reduce Motion: анимации не для всех
Часть пользователей включает Reduce Motion из-за вестибулярных расстройств — параллакс и интенсивные анимации буквально вызывают у них головокружение.
Проверяйте настройку и упрощайте анимации:
if UIAccessibility.isReduceMotionEnabled {
// Простой fade вместо сложной анимации
UIView.animate(withDuration: 0.2) {
view.alpha = targetAlpha
}
} else {
// Полная анимация с движением
performFullScreenTransition()
}
В SwiftUI используйте .animation() с условием:
@Environment(\.accessibilityReduceMotion) var reduceMotion
var body: some View {
content
.animation(reduceMotion ? .none : .spring(), value: isVisible)
}
Touch Targets: минимум 44×44 pt
Apple рекомендует минимальный touch target 44×44 pt. Это не значит, что иконка должна быть такого размера — можно расширить зону касания через contentEdgeInsets или кастомный hitTest:
// UIKit — расширяем зону касания
class LargerTouchButton: UIButton {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let expandedBounds = bounds.insetBy(dx: -10, dy: -10)
return expandedBounds.contains(point)
}
}
В SwiftUI:
Button(action: action) {
Image(systemName: "xmark")
.frame(width: 20, height: 20)
}
.contentShape(Rectangle().size(CGSize(width: 44, height: 44)))
Маленькие кнопки — одна из самых частых жалоб пользователей с моторными ограничениями. И не только у них: попробуйте нажать кнопку 20×20 pt в переполненном автобусе.
Switch Control и другие функции ввода
Switch Control позволяет управлять iPhone через внешний свитч, нажатие кнопки или даже через камеру (по движению головы). Пользователи с тяжёлыми моторными ограничениями полностью зависят от него.
Для Switch Control важно:
- Правильный порядок фокуса (тот же, что для VoiceOver)
- Каждое значимое действие должно быть доступно через клавиатуру или через пользовательские действия accessibility
Custom actions — мощный инструмент, позволяющий добавить дополнительные действия к элементу без загромождения интерфейса:
let editAction = UIAccessibilityCustomAction(
name: "Редактировать",
target: self,
selector: #selector(editItem)
)
let deleteAction = UIAccessibilityCustomAction(
name: "Удалить",
target: self,
selector: #selector(deleteItem)
)
cell.accessibilityCustomActions = [editAction, deleteAction]
Pользователь VoiceOver увидит эти действия через свайп вверх/вниз. Ячейка таблицы с кнопками "редактировать" и "удалить" не обязательно должна содержать видимые кнопки — custom actions покрывают этот сценарий чисто.
Тестирование: как проверить всё это
Accessibility Inspector (Xcode → Open Developer Tool) — запускается на симуляторе, показывает все accessibility-атрибуты элементов, умеет проверять цветовой контраст и запускать аудит.
VoiceOver на реальном устройстве — тройной клик, закрываете глаза и пытаетесь выполнить ключевые сценарии. Если вы потерялись — пользователь потеряется тоже.
Accessibility Audit в UI Tests:
func testAccessibility() throws {
let app = XCUIApplication()
app.launch()
try app.performAccessibilityAudit()
}
performAccessibilityAudit() появился в iOS 17 и автоматически проверяет распространённые проблемы: элементы без label, маленькие touch targets, проблемы с контрастом.
Размеры шрифта — в симуляторе идите в Настройки → Универсальный доступ → Размер текста и тяните ползунок в максимум. Если UI ломается — нужно фиксить.
С чего начать, если проект уже большой
Не нужно переделывать всё за один спринт. Приоритизируйте:
- Критичные flow: онбординг, авторизация, основная функциональность. Если пользователь не может войти в приложение — остальное неважно.
- Иконки и кнопки без label — это быстрые фиксы с большим влиянием.
- Цветовой контраст — часто решается через дизайн-токены, не требует переработки кода.
- Dynamic Type — убираем захардкоженные высоты строк и размеры шрифта.
Поэтапный подход работает лучше перфекционизма. Приложение с базовым VoiceOver на ключевых экранах лучше, чем приложение без него вообще.
Команда REEXY при разработке iOS-приложений закладывает accessibility в процесс с нуля — это проще и дешевле, чем добавлять потом. Если вам нужна консультация или аудит текущего проекта, свяжитесь через r3xy.ru.
Пара слов про документацию
Apple ведёт отличную документацию по теме: Human Interface Guidelines отдельно описывают принципы доступности, а в Developer Documentation есть подробный раздел Accessibility. Ещё полезен WWDC-контент — каждый год выходят сессии про новые возможности.
WCAG (Web Content Accessibility Guidelines) формально про веб, но принципы применимы и к мобильным. Уровень AA — разумная цель для большинства приложений.
Accessibility — это не сложно технически. Это сложно культурно: нужно помнить об этом на этапе проектирования, а не добавлять в последний момент перед релизом. Если вы начнёте с правильных label для иконок, Dynamic Type и нормального контраста — вы уже в топ-20% приложений в App Store по доступности.