VoiceOver на iOS: каждый контрол ведёт себя по-разному
Привет, Хабр! Недавно я говорил про адаптацию приложений для незрячих и неподвижных людей. И не договорил!
Сегодня расскажу, как изменить поведение контролов с помощью accessibilityTraits
и сделать жизнь незрячих чуть удобней. Знать работу этих трейтов (traits) важно, чтобы не писать свои костыли.
В первой части мы начали разбираться с адаптацией приложений для незрячих с помощью VoiceOver: подписали контролы, сгруппировали их, исправили навигацию. В этой статье пойдём дальше и рассмотрим «особенности», которые можно дать контролам, чтобы улучшить их работу для незрячих людей и в целом повысить удобство использования приложения.
Особенности элементов управления — Trait collection
VoiceOver имеет стандартный набор «особенностей» UITraitCollection
, которые вы можете применять к контролам. Важно знать о них заранее, чтобы не придумывать своих решений. Я поделил их на три типа:
- Тип контрола.
- Состояние контрола.
- Особые свойства контролов.
Сразу буду показывать на примере экрана с карточкой пиццы:
Тип контрола
VoiceOver знает про несколько базовых типов элементов. Часть из них уже настроена в вашем проекте, но всё равно расскажу про них.
Типы контролов используются для навигации: по ним можно быстро перемещаться с помощью ротора.
-
.staticText
— для надписей, которые не меняются. Текст просто прочитается. -
.header
— заголовок: Добавить в пиццу, заголовок. -
.button
— кнопка. Основной способ подписывать активные контролы: Изменить состав, кнопка. -
.image
— картинка. -
.link
— ссылка. Редкий гость в приложениях, частый на сайтах. -
.searchField
— поиск.
Смотрим на примере:
- Указываем заголовок.
.staticText
ставится автоматически для всех надписей, а вот.header
для заголовка нужно поставить вручную. При этом нужен и.header
и.staticText
. - Отмечаем место под картинку. В прошлый раз все маленькие картинки мы скрыли от 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 шага:
- Сделать контейнер с кнопками доступным.
- Поставить трейт
.adjustable
. - Реализовать методы увеличения и уменьшения.
- Возвращать новое значение для
.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 (Нижний Новгород).