VoiceOver на iOS: каждый контрол ведёт себя по-разному

    Привет, Хабр! Недавно я говорил про адаптацию приложений для незрячих и неподвижных людей. И не договорил!

    Сегодня расскажу, как изменить поведение контролов с помощью accessibilityTraits и сделать жизнь незрячих чуть удобней. Знать работу этих трейтов (traits) важно, чтобы не писать свои костыли.



    Адаптация iOS-приложения — большая тема, в одну статью всё не влезло, поэтому выпускаю их серией.

    1. Voice Control и VoiceOver: как адаптировать приложение для незрячих или неподвижных.
    2. VoiceOver на iOS: каждый контрол ведёт себя по-разному.
    3. VoiceOver на iOS: решение типовых проблем.
    4. Разница между реализацией VoiceOver, Voice Control и UI тестов. (In progress)

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

    Особенности элементов управления — Trait collection


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

    1. Тип контрола.
    2. Состояние контрола.
    3. Особые свойства контролов.

    Сразу буду показывать на примере экрана с карточкой пиццы:



    Тип контрола


    VoiceOver знает про несколько базовых типов элементов. Часть из них уже настроена в вашем проекте, но всё равно расскажу про них.

    Типы контролов используются для навигации: по ним можно быстро перемещаться с помощью ротора.

    • .staticText— для надписей, которые не меняются. Текст просто прочитается.
    • .header — заголовок: Добавить в пиццу, заголовок.
    • .button — кнопка. Основной способ подписывать активные контролы: Изменить состав, кнопка.
    • .image — картинка.
    • .link — ссылка. Редкий гость в приложениях, частый на сайтах.
    • .searchField — поиск.

    Смотрим на примере:


    1. Указываем заголовок. .staticText ставится автоматически для всех надписей, а вот .header для заголовка нужно поставить вручную. При этом нужен и .header и .staticText.
    2. Отмечаем место под картинку. В прошлый раз все маленькие картинки мы скрыли от VoiceOver, информативность не потеряли. В этот раз картинка большая, ее так просто не скрыть: место станет пустым, это странно. Помечаем картинку как .image и подписываем .accessibilityLabel = "Пицца Пепперони Фреш с перцем".

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

    Состояние контрола


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



    • .selected — добавляет «выбрано» перед названием контрола. Подходит для всех свитчеров и чекбоксов.
    • .notEnabled — добавляет «недоступно». Эта настройка не видна в Interface Builder и управляется только программно.

    Пример с добавлением топпингов в пиццу:


    С помощью состояний можно объяснить пользователю, что топпинг добавлен. Удобно поправить прямо внутри ячейки. accessibilityTraits это OptionSet, поэтому к нему можно применять методы вставки .formUnion и удаления .formIntersection:

    class ToppingCell: UICollectionViewCell {
        override var isSelected: Bool {
            didSet {
                if isSelected {
                    accessibilityTraits.formUnion(.selected)
                } else {
                    accessibilityTraits.formIntersection(.selected)
                }
            }
        }
        ...
    }

    Особые свойства контролов


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

    • .summaryElement — первое, что скажет приложение после запуска. Например, приложение погоды после запуска может сразу рассказать о температуре, а музыкальный плеер расскажет о включенной песне и исполнителе. В нашем случае можно говорить статус доставки, если заказа уже оформлен.
    • .updatesFrequently — штука для таймеров. Новое значение будет проговариваться раз в несколько секунд.
    • .causesPageTurn — скролит после прочтения. Вызовется accessibilityScroll(.next) у того контрола, который сможет это обработать. Смотрит по .firstResponder.
    • .startsMediaSession — обычно VoiceOver повторяет название нажатой кнопки, чтобы подтвердить действие. Это мешает, если контрол проигрывает звук. Включите этот трейт, чтобы VoiceOver не повторял название контрола.
    • .playsSound — стоит включить, если вы проигрываете собственный звук при фокусировании на контроле (если я всё правильно понял, сам ни разу не использовал).
    • .allowsDirectInteraction — для рисования и обработки жестов. Контрол сразу обрабатывает касание, будто VoiceOver выключен.
    • .keyboardKey — контрол начинает реагировать, как кнопка на клавиатуре. У VoiceOver есть несколько режимов ввода текста для таких случаев:

      standart typing — как простая кнопка в VoiceOver: сначала наведите фокус на букву, а затем нажмите дважды в любом месте, чтобы написать её. Набирать можно быстрее двумя руками: одним пальцем водить по клавиатуре (буквы будут озвучиваться) и касаться другим пальцем, чтобы подтвердить выбор клавиши.
      touch typing — однорукий ускоренный набор: водите пальцем по клавиатуре, чтобы озвучить кнопки. Отпустите палец, чтобы написать букву.
      direct touch typing — как обычный набор, будто VoiceOver выключен.

    Видео про разные способы ввода:


    Достаточно поставить галочку в IB, чтобы добавить поведение. С трейтом .adjustable так просто не получится, о нём отдельно.

    Настраиваем могучий трейт .adjustable


    И последний, особенно важный трейт .adjustable — элемент, который можно регулировать: так работают UIStepper и UISlider. Свайпните такой контрол вверх или вниз, чтобы изменить значение (не забывайте, что свайп влево/вправо переключит фокус на соседний элемент). Если у контрола есть UIPanGestureRecognizer, то можно тапнуть дважды и задержать второй тап, так жест сработает и можно управлять им напрямую, будто VoiceOver выключен.

    Примеры применений для .adjustable:

    Переключатель теста. Настройка теста состоит из пяти кнопок: три для выбора размера пиццы и две для типа теста. Их стоит сгруппировать и подписать, чтобы вместо пяти осталось две: «Размер, средний. Элемент регулировки» и «Тесто, традиционное. Элемент регулировки».



    Нужно сделать в 4 шага:

    1. Сделать контейнер с кнопками доступным.
    2. Поставить трейт .adjustable.
    3. Реализовать методы увеличения и уменьшения.
    4. Возвращать новое значение для .accessibilityValue.

    override public func awakeFromNib() {
            super.awakeFromNib()
            
            isAccessibilityElement = true // 1
            accessibilityTraits = .adjustable // 2
        }


    extension SegmentedControl {
        override public func accessibilityIncrement() { // 3
            controller.selectNext(increment: +1)
        }
        
        override public func accessibilityDecrement() { // 3
            controller.selectNext(increment: -1)
        }
        
        public override var accessibilityValue: String? { // 4
            get {
                return selectedSegment?.accessibilityValue
            } set { }
        }
    }
    

    Теперь после свайпа вверх вызовется accessibilityIncrement(), вы увеличите внутренний счётчик, и VoiceOver прочитает новое значение из accessibilityValue.

    Счетчик количества всего на свете. В данном блоке мы видим четыре контрола: кнопка минус, количество, кнопка плюс и цена. Можно объединить их в одну view и превратить в один контрол: «Количество, 1, 575 рублей. Элемент регулировки». После вертикального свайпа изменится количество, а затем произнесётся новое значение вместе с ценой.



    Горизонтальные UICollectionView. Оказалось, что .adjustable удобно применять и для горизонтальных UICollectionView. Например, выбрать акцию в меню или машину в такси.



    Заключение


    В этот раз мы разобрали трейты: их типы, состояния и поведение. Это стандартный набор для типовых задач. Для сложных контролов можно использовать .adjustable.

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

    Чтобы не пропустить следующую статью, подписывайтесь на мой канал Dodo Pizza Mobile.

    А ещё у нас сейчас открыта одна вакансия в мобильном направлении. Так что я просто оставлю это здесь: Senior iOS Developer (Нижний Новгород).

    Dodo Pizza Engineering
    О том как IT доставляет пиццу

    Комментарии 7

      0
      Помечаем картинку как .image и подписываем .accessibilityLabel = «Пицца Пепперони Фреш с перцем».


      а с заголовком под картинкой не конфликтует?
        +1

        Немного конфликтует. Выходов несколько.


        • Целиком скрыть картинку. Не смотря на проблему пустого места, ценности у картинки для незрячего немного.
        • Кратчайшим образом подписать картинку, например, «пицца».
        • Оставить полное название в картинке, но скрыть от VoiceOver текст с заголовком.

        Еще можно пропустить картинку и поставить фокус на заголовок при открытии экрана через UIAccessibility.postNotification. Об этом будет в следующей статье :-)


        В любом случае, это не будет большой проблемой для незрячего. Чуть неудобно, но не более.

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

            А вот это точно не стоит делать. В скрин ридерах есть выбор навигации, например: по символам, по словам, по заголовкам, по абзацам, и так далее. Навигация по заголовкам очень удобная и незрячие ею очень часто пользуются. Это касается и любых сайтов. Очень удобно прыгнуть по заголовку и попасть сразу на начало статьи, например. И не листать все меню, баннеры и прочие надстройки. И очень опечаливают те сайты, где не используют заголовки настоящие, которые тегом html идут, а делают обычный текст и просто свойствами CSS выделяют его. Крайне неудобно пользоваться такими сайтами. Ну обычно их сразу и покидают если есть более удобные альтернативы.
              0

              Согласен, спасибо.

          0
          Может подскажете, как вызвать произношение accessibilityHint при использовании VoiceOver? Почему-то в моем случае эти хинты не произносились.
            0

            В настройках они включены?
            Универсальный доступ → VoiceOver → Детализация → Подсказки

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

          Самое читаемое