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

от автора

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

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



В первой части мы начали разбираться с адаптацией приложений для незрячих с помощью 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 (Нижний Новгород).

ссылка на оригинал статьи https://habr.com/ru/company/dodopizzadev/blog/488246/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *