По данным 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 читает интерфейс:

  1. Он проходит по элементам в порядке, определённом accessibility frame
  2. Для каждого элемента читает label, value и hint
  3. Пользователь свайпает вправо/влево для навигации, двойной тап — действие

Основные атрибуты, которые вы контролируете:

// Задать 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 — система масштабирования текста. Пользователи выбирают предпочтительный размер в Настройках, и приложения должны уважать этот выбор.

Правила простые:

  1. Используйте системные text styles вместо хардкода размеров шрифта
  2. Никогда не фиксируйте высоту строк и высоту ячеек
  3. Тестируйте на размере 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 ломается — нужно фиксить.

С чего начать, если проект уже большой

Не нужно переделывать всё за один спринт. Приоритизируйте:

  1. Критичные flow: онбординг, авторизация, основная функциональность. Если пользователь не может войти в приложение — остальное неважно.
  2. Иконки и кнопки без label — это быстрые фиксы с большим влиянием.
  3. Цветовой контраст — часто решается через дизайн-токены, не требует переработки кода.
  4. 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 по доступности.