Доброго времени суток! Меня зовут Иван Смолин. Я iOS разработчик в Touch Instinct.


Сегодня я хочу рассказать, что из себя представляет технология VoiceOver в iOS. И как мы сделали футбольное приложение более удобным для людей, у которых есть нарушения зрения.


Что такое VoiceOver


Это основанный на жестах способ чтения содержимого на экране мобильного устройства. Технология позволяет пользователям управлять своим мобильным устройством без необходимости видеть содержимое на экране. Она выступает в качестве посредника между интерфейсом приложения и пользователем, проговаривает вслух детали элементов интерфейса и действия в приложении.


Эта функция особенно полезна для людей с нарушениями зрения. VoiceOver упрощает использование мобильного устройства для людей с такого рода проблемами. Если эта функция активирована, пользователи могут ориентироваться в интерфейсе и понимать, какие действия нужно сделать и какими будут результаты этих действий. [1]


Как пользоваться VoiceOver


Когда пользователь тапает по любому месту на экране, его мобильное устройство (iPhone, iPad, Watch) проговаривает вслух элемент, который находится в этом месте.


  • При двойном тапе по элементу происходит его выделение;
  • Чтобы проскролить контент на экране, нужно воспользоваться тремя пальцами вместо одного;
  • Двойной тап тремя пальцами включит или выключит голосовые команды на экране блокировки;
  • Двойной тап двумя пальцами во время выполнения какого либо действия остановит это действие либо запустит его, если оно не было запущено. Например, во время телефонного разговора двойной тап двумя пальцами на экране завершит звонок

Как добавить поддержку VoiceOver в ваше приложение


В подавляющем большинстве случаев ваше приложение уже будет хорошо работать с VoiceOver. Тут стоит отдать должное инженерам Apple — все стандартные компоненты полностью поддерживают технологию VoiceOver. Однако, иногда всё же нужно написать немного дополнительного кода, чтобы ваш интерфейс стал более удобным для людей с нарушениями зрения.


Бывают всего два типа компонентов, для которых нужно обязательно реализовывать поддержку VoiceOver самостоятельно:


  1. Графический элемент: график, диаграмма, изображение.
  2. Сложный элемент, изначально никак не адаптированный для людей с нарушениями зрения: например, таблица с заголовками в виде аббревиатур.

Так как первый вид встречается довольно редко, мы рассмотрим подробно способы, которые позволят значительно улучшить ситуацию во втором случае.


В остальных случаях можно лишь повысить удобство использования при помощи дополнительных возможностей, которые предоставляет нам VoiceOver.


Дизайн


Если в приложении изначально закладывается поддержка вспомогательных технологий, таких как VoiceOver, то всё начинается с дизайна. Существует специальное руководство по обеспечению доступности веб-контента и руководство, как применять этот стандарт для мобильных платформ. Эти руководства стоит как минимум прочитать перед тем, как начинать рисовать дизайн.


После того, как макеты нарисованы, к ним необходимо приложить дополнительную информацию. Она не видна визуально, но имеет критическое значение для людей с нарушениями зрения. В случае VoiceOver такой информацией будет являться текстовое описание всех элементов экрана, с которыми пользователь может взаимодействовать: видеть или нажимать. Важно текстово описать их характеристики.


Пример


Дизайн экрана логин с дополнительной информацией для VoiceOver

Переключать «запомнить меня» (4) на странице входа.


  • Текст, который будет прочитан в первую очередь: «Запомни меня»
  • Тип элемента: «Переключатель»
  • Текущее значение: «Включен или выключен»
  • Дополнительная подсказка: «Нажмите два раза, чтобы запомнить логин»

Разработка


После получения локализованных строк можно начинать разработку. Для начала необходимо прочитать руководство по программированию специальных возможностей. А затем уже углубиться в UIAccessibility API.


UIAccessibility


Этот протокол содержит в себе набор свойств, которые описывают пользовательские элементы интерфейса. Вспомогательные приложения используют эту информацию, чтобы помочь пользователям с ограниченными возможностями.
Этот протокол содержит очень много свойств, которые можно сгруппировать по критериям:


  1. Отвечающие за описание элемента:


    • isAccessibilityElement — свойство определяет, будет ли этот элемент доступен для выделения и проговаривания через VoiceOver;
    • accessibilityLabel — краткое описание содержимого в элементе, например текст на кнопке. Этот текст будет прочитан в первую очередь;
    • accessibilityTraits — комбинация (перечисление) характеристик, которыми обладает элемент. Эти характеристики помогают VoiceOver понять, как нужно взаимодействовать с элементом. Характеристик достаточно много [https://developer.apple.com/reference/objectivec/nsobject/1615202-accessibilitytraits], перечислим только некоторые их них.


      • Наиболее часто используемые:
        • UIAccessibilityTraitButton — указывает, что элемент должен восприниматься VoiceOver как кнопка;
        • UIAccessibilityTraitLink — указывает, что элемент должен восприниматься VoiceOver как ссылка;
        • UIAccessibilityTraitImage — указывает, что элемент должен восприниматься VoiceOver как изображение;
        • UIAccessibilityTraitSelected — указывает, что элемент находится в выбранном состоянии.
      • Редко используемые, но интересные:
        • UIAccessibilityTraitCausesPageTurn — указывает, что элемент должен перевернуть страницу когда VoiceOver заканчивает читать текст на ней;
        • UIAccessibilityTraitSummaryElement — указывает, что элемент предоставляет резюме того, что отображено на экране. Например, текущая температура в приложении Weather;
        • UIAccessibilityTraitStartsMediaSession — указывает, что этот элемент запускает медиа-сессию, когда он активирован. Например, это заглушает голос VoiceOver, когда пользователь записывает аудио.

    • accessibilityValue — текущее значение в элементе. Например, это может быть значение в слайдере или текст в текстовом поле. Этот текст будет прочитан после текста, присвоенного в accessibilityLabel;
    • accessibilityHint — краткое описание того, что произойдет как только пользователь выполнит какое-либо действие, ассоциированное с этим элементом. Этот текст будет прочитан после текста, присвоенного в accessibilityValue, либо после accessibilityLabel, если значение в accessibilityValue не установлено;
    • accessibilityLanguage — язык, который должен быть использован для проговаривания текста из свойств accessibilityLabel, accessibilityValue и accessibilityHint.

  2. Свойства, отвечающие за пространственные характеристики элемента:
    • accessibilityActivationPoint — точка активации элемента на экране;
    • accessibilityFrame — место на экране на котором располагается элемент;
    • accessibilityPath — линия выделения элемента вместо обычного прямоугольника.
  3. Свойства, отвечающие за компоновку:
    • accessibilityElementsHidden — нужно ли прятать дочерние «доступные» элементы, находящиеся внутри родительского.
    • shouldGroupAccessibilityChildren — нужно ли группировать дочерние «доступные» элементы.
    • accessibilityViewIsModal — нужно ли игнорировать другие элементы, находящиеся на одном уровне иерархии с текущим.
  4. Свойства, отвечающие за навигацию:
    • accessibilityNavigationStyle — описывает, как должна происходить навигация по дочерним элементам контейнера.

Если в вашем приложении есть проблемы с доступностью для людей с ограниченными возможностями, то чаще всего требуется правильно переопределить только первую группу свойств.


Пример UIAccessibility в FotMob


Теперь я приведу пример, как, используя вышеописанные свойства UIAccessibility, решить вполне конкретные проблемы в приложении.


В ходе тестирования был выявлен ряд недочётов, связанных с именованием кнопок в навбаре для VoiceOver. Мы решили порефакторить код создания кнопок, чтобы решить эти проблемы и сделать кнопки переиспользуемыми во всём приложении.


Для начала мы создали enum со списком всех кнопок и значениями, которые эти кнопки могут в себе хранить:


enum Button {

    case calendar(date: Date)
    case filter(isActive: Bool)
    case alert(isActive: Bool)
    case commentary
    case share
    case favorite(isSelected: Bool)
    case addToCalendar
    case edit
    case close

}

Далее был создан extension, который создавал готовые инстансы UIBarButton и UIButton из значений enum’а:


Extension к Button
extension Button {

    private var icon: UIImage? {
        switch self {
        case .calendar(let date):
            let icon = R.image.ic_calendar()

            let dayOfMonth = // * get day of month * //

            let iconWithDate = //* render icon with day of month number *//

            return iconWithDate
        case .filter(let isActive):
            return isActive ? R.image.filter_active() : R.image.filter_inactive()
        case .alert(let isActive):
            return isActive ? R.image.notification_bell_title_bar_active() : R.image.notification_bell_title_bar_inactive()
        case .commentary:
            return R.image.icon_audio_commentary_off()
        case .share:
            return R.image.icon_share()
        case .favorite(let isSelected):
            return (isSelected ? R.image.starOn() : R.image.starOff())?.withRenderingMode(.alwaysTemplate)
        case .addToCalendar:
            return R.image.icon_calendar()
        case .edit:
            return R.image.edit()
        case .close:
            return R.image.close()
        }
    }

    func buttonWith(target: Any, action: Selector, buttonSize: ButtonSize = .iconSize) -> UIButton {
        let button = UIButton()
        button.setImage(icon, for: .normal)
        button.addTarget(target, action: action, for: .touchUpInside)

        let size: CGSize

        switch buttonSize {
        case .custom(let customSize):
            size = customSize
        case .iconSize:
            size = icon?.size ?? .zero
        }

        button.frame = CGRect(.zero, size)

        return button
    }

    func barButtonWith(target: Any, action: Selector, buttonSize: ButtonSize = .iconSize) -> UIBarButtonItem {
        let item = UIBarButtonItem(customView: buttonWith(target: target, action: action, buttonSize: buttonSize))

        switch self {
        case .calendar(let date):
            let dateFormatter = DateFormatter()
            dateFormatter.dateStyle = .medium

            item.accessibilityLabel = R.string.localizable.calendar()
            item.accessibilityValue = dateFormatter.string(from: date)
            item.accessibilityHint = R.string.localizable.select_date_to_filter_matches_hint()
        case .filter(let isActive):
            item.accessibilityLabel = R.string.localizable.filter()
            item.accessibilityValue = isActive ? R.string.localizable.active() : R.string.localizable.inactive()
        case .alert(let isActive):
            item.accessibilityLabel = R.string.localizable.set_alerts()
            item.accessibilityValue = isActive ? R.string.localizable.alerts_enabled() : R.string.localizable.alerts_disabled()
            item.accessibilityHint = R.string.localizable.edit_match_alerts()
        case .commentary:
            item.accessibilityLabel = R.string.localizable.commentary_window_title()
        case .share:
            item.accessibilityLabel = R.string.localizable.share()
        case .favorite(let isSelected):
            item.accessibilityLabel = R.string.localizable.toggle_favorite()
            item.accessibilityValue = isSelected ? R.string.localizable.in_favorites() : R.string.localizable.not_favorites()
        case .addToCalendar:
            item.accessibilityLabel = R.string.localizable.add_to_calendar()
        case .edit:
            item.accessibilityLabel = R.string.localizable.edit()
        case .close:
            item.accessibilityLabel = R.string.localizable.close()
        }

        return item
    }

}

Создание кнопок в итоге выглядело вот так:


class SomeController {

    private let navBarButtons: [Button] = [.commentary, .share, .addToCalendar]

    private var navBarButtonItems: [UIBarButtonItem] {
        return navBarButtons.flatMap {
            switch $0 {
            case .commentary:
                return $0.barButtonWith(target: self,
                                        action: #selector(self.onBtnCommentaryClicked),
                                        buttonSize: .rightBarButtonSize)
            case .share:
                return $0.barButtonWith(target: self,
                                        action: #selector(self.onBtnShareMatchClicked),
                                        buttonSize: .rightBarButtonSize)
            case .addToCalendar:
                return $0.barButtonWith(target: self,
                                        action: #selector(self.onBtnAddMatchToCalendarClicked),
                                        buttonSize: .rightBarButtonSize)
            default:
                return nil
            }
        }
    }

    func updateBarButtons() {
          navItem.setRightBarButtonItems(navBarButtonItems, animated: false)
    }
}

После этих изменений в приложении появились не только правильные названия для VoiceOver, но и добавилась дополнительная информация, которая очень полезна для людей с нарушениями зрения. Например, при выборе кнопки календаря, прочитывается не только его название, но и выбранная дата.


Инспектирование accessibility свойств кнопки календарь через Acessibility Inspector

Аналогично для кнопки уведомлений. Мы сообщаем пользователю, находится ли кнопка в выбранном состоянии (уведомления включены) или нет. Также мы сообщаем пользователю, что случится, если он тапнет по той или иной кнопке.


Инспектирование accessibility свойств кнопки уведомления через Acessibility Inspector

Такие небольшие улучшения значительно упрощают использование приложения.


Другим проблемным кейсом в приложении была таблица со статистикой команд. Для VoiceOver такая таблица представляет из себя просто набор аббревиатур и цифр, которые никак не связаны. Если мы попытаемся запустить приложение с включенным VoiceOver, то сразу станет понятно, что незрячим людям разобраться с этим набором бессмысленных слов просто невозможно.


Таблица статистики по командам в приложении FotMob

Здесь мы использовали группировку элементов и специально подготовленные для VoiceOver строки. Например, заголовок таблицы теперь читается не как «PL W D L ± GD PTS», а в развёрнутом виде: «Table header: Played, Won, Drawn, Lost, Goal difference, Goal difference value, Points».


Каждая строка таблицы, как и заголовок, включает в себя полное описание полей: «Position: 4, Manchester United, Played: 36, Won: 21, Drawn: 9, Lost: 6, Goal difference: 72-38, Goal difference value: 34, Points: 72»


Тестирование


Если у вас есть матёрые тестировщики, которые умеют тестировать с закрытыми глазами — вам повезло. Однако часто ответственность за проверку функциональности ложится на плечи программиста.


Симулятор


Несмотря на то, что VoiceOver не поддерживается в симуляторе, тестировать приложение можно и на нём. В этом нам помогает утилита Accessibility Inspector, которая устанавливается вместе с Xcode.


В этой утилите можно:


  1. Найти общие проблемы с доступностью вашего приложения;
  2. Увидеть атрибуты доступности каждого элемента в режиме проверки;
  3. Изменить общие настройки доступности и проверить, как они повлияют на приложение без его перезапуска.

По своему опыту могу сказать, что этой утилиты вполне достаточно для разработки, но недостаточно для полноценного тестирования. Так как она не проговаривает тексты и мы можем упустить очень важные проблемы, связанные со знаками препинания.


Тестировщики


Лучше всего, когда у вас есть человек с нарушениями зрения, который может по-настоящему попробовать приложение с включенным VoiceOver. Нам повезло, у нас был неравнодушный пользователь, который прошёлся по всему приложению и составил детальное описание всех мест, где VoiceOver работает недостаточно хорошо. Мы смогли исправить все указанные недочёты. Он и дальше помогал нам улучшать доступность приложения, за что ему огромное спасибо.


Забота о пользователях


VoiceOver является продвинутой технологией, которая позволяет вам делать по-настоящему доступные приложения для людей с нарушениями зрения.


Она встроена во все операционные системы Apple и работает фактически «из коробки». Поэтому независимо от того, пользуетесь ли вы VoiceOver или разрабатываете приложение с его поддержкой, VoiceOver везде будет работать одинаково и предсказуемо.


Качественно сделанные приложения с поддержкой VoiceOver могут даже удостоиться премии iOS Design Awards, как это случилось с Workflow в 2015 году.


The Workflow app was selected for an Apple Design Award in 2015 because of its outstanding use of iOS accessibility features, in particular an outstanding implementation for VoiceOver with clearly labeled items, thoughtful hints, and drag/drop announcements, making the app usable and quickly accessible to those who are blind or low-vision. [2]

Наверное, каждая команда хотела бы получить такой же отзыв и о своём приложении. Старайтесь сделать всё возможное для ваших пользователей, чтобы им было удобно пользоваться приложением вне зависимости от их возможностей.


Дополнительные материалы на тему VoiceOver


Поделиться с друзьями
-->

Комментарии (1)


  1. alekssamos
    26.05.2017 15:48

    Вот как раз у меня нет зрения и я пользуюсь Voiceover. Да, заметил, что есть приложения, которые не доступны. И надо писать об этом чаще, чтобы всё больше разработчиков обращали на это внимание.