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

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

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

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

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


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

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


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

ogo3hyitv8vbtt48h8bfn3xkfog.png

Тип контрола


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

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

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


Смотрим на примере:
iw41zsnwe9ovz5nlcls8uhpkoii.png

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


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

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


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

q0hdy3gmrgvjjkkr9oqjg4saite.png

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


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

С помощью состояний можно объяснить пользователю, что топпинг добавлен. Удобно поправить прямо внутри ячейки. 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:

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

uzveg5hrmowslt8kjv_ozqr3iy8.jpeg

Нужно сделать в 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 рублей. Элемент регулировки». После вертикального свайпа изменится количество, а затем произнесётся новое значение вместе с ценой.

shamfc7amhzyby74kckmo1b8o0a.png

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

x0gdcogxze0krken-2owluyklpa.jpeg

Заключение


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

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

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

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


© Habrahabr.ru