Как стать автором
Обновить
Dodo Engineering
О том, как разработчики строят IT в Dodo

Заголовок будет другой

Время на прочтение 8 мин
Количество просмотров 9.3K
Если вы разрабатываете продукт для масс-маркета, то вероятнее всего им пользуются люди с плохим зрением. Если вы стремитесь делать удобные интерфейсы, то надо сделать удобно для всех клиентов, в том числе для людей с плохим зрением. Думаю, мы часто забываем об этом. И это пора исправлять.



Я ввёл в поиске в App Store запрос «доставка пиццы», скачал первые 24 приложения и проверил, кто из них предоставляет интерфейс для людей с плохим зрением. 



2 из 24. Причем один из двух, кажется, сделал это случайно: при увеличении размера шрифта весь интерфейс «плывёт» и им становится пользоваться только сложнее. Печально.

iOS-приложением Додо Пиццы ежемесячно пользуются 550 000 человек. Даже если у 1% наших пользователей включен увеличенный шрифт, то это 5500 человек, которым некомфортно пользоваться нашим приложением. Будем исправлять.

Добавляем поддержку Dynamic Type


  1. Используем динамические системные текстовые стили вместо статичных.
  2. По желанию, включаем в сторибордах галочку Automatically Adjusts Font у лейблов. Или, если лейбл в кнопке или создаётся через код,  стучимся в его параметр adjustsFontForContentSizeCategory.
  3. Учим интерфейс растягиваться под разные размеры шрифтов:
    — Используем автоматический расчёт размеров ячеек, где можем.
    — Где не можем — получаем актуальные настройки размера шрифта и реагируем на изменения в методе traitCollectionDidChange.
  4. Получаем интерфейс, которым невозможно пользоваться.




Меняем интерфейс, чтобы им стало можно пользоваться


Откатываемся назад и начинаем думать, как всё сделать хорошо.

Грамотно используем место в меню


Сейчас под картинкой пиццы много пустого места. Попробуем поставить картинку над названием: так она станет больше, а пустое место пропадёт. Для этого мы завернём в UIStackView картинку и вью-контейнер со всем остальным, а затем будем переключать направление стаквьюхи при необходимости.



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



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

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



Вот теперь то.

Промежуточный итог: используем больше места, глаза меньше прыгают со строки на строку, читать стало легче. 

Улучшаем растягивая и убирая


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



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



В целом, меню без фоток не особо потеряло в информативности, зато теперь одна позиция меню почти всегда влазит в экран айфона 6S. Но стало менее привлекательно, СЛЮНКИ НЕ ТЕКУТ ПРИ СКРОЛЛЕ. Такое. Пока что оставим так, хорошенько подумаем и, может быть, попозже всё же вернём картинку.

Не забываем проверять «вживую»


Теперь категории. В целом, ещё при первом подходе получилось сносно. Наворачиваем по новой.



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

Давайте заменим UICollectionView на кнопку, которая будет вызывать UIActionSheet.



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

Не забываем про очень длинные строки


Сначала возьмёмся за выбиралку города. Со шрифтом в кнопке ничего сложного нет, а вот научить «треугольничек» расти вместе со шрифтом — интересно. В нашем случае треугольничек был сделан иконкой в кнопке, которая передвинута на правую сторону через CGAffineTransform. Ещё как вариант — собирать NSAttributedString из текста и иконки треугольничка, а потом всё это скормить кнопке. Чтобы иконка нормально скейлилась можно использовать векторную картинку, которая должна обязательно лежать в ассетах с галочкой Preserve Vector Data.


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

Теперь растягиваем додо-рубли, тут всё просто:



А вот теперь вопрос: что будет, если название города окажется длинным и у нас будет много додо-рублей? По идее, нужно сократить название города. Помните, что я говорил о втором варианте добавления такой иконки в кнопку, через NSAttributedString? Я попробовал и теперь возникла проблема, что при сокращении заголовка у нас и иконка треугольника пропадает, ведь она теперь часть заголовка. Штош. Придётся возвращать логику передвигания иконки через трансформы. 
Если вы знаете удобный способ как передвинуть иконку в кнопке на правую сторону и скейлить её вместе со шрифтом в заголовке — скиньте в комменты, пожалуйста.

Впихиваем невпихиваемое


Наконец-то акции. Тут надо сесть и подумать. Заголовок может быть длинный и даже сейчас он иногда не влазит в одну строку. На большом кегле он не влезет ну вообще никак. Если сделать верхнюю оранжевую панель резиновой и позволить заголовку акции в большом кегле занимать несколько строк, то верхний блок отъест половину экрана даже на больших айфонах, а про 4S вообще можно будет не вспоминать. Это не дело. Можно поиграть с лейаутом внутри ячейки акции: сделать картинку квадратной, а освободившееся место занять заголовком. Но картинки для акций подгоняются под конкретный формат и будут некорректно показываться в другом. Так нельзя.

Сложна.

Так, а можно ж опять полностью убрать картинки и всё место занять заголовком.



Ага, оно. Руки чешутся раскрасить фон под заголовком акции, но это плохо скажется на читаемости. А мы, вроде как, улучшить её пытаемся. Так что ничего не красим и идём дальше, к оставшимся двум кнопкам про адрес и промокоды.

Работаем с жесткими ограничениями


Заголовки в этих кнопках — несокращаемые. Но если их не сокращать, то кнопки наползут друг на друга. И да, спрятать эти кнопки нельзя.

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



Уффф, всё. Насчёт выключенных фотографий в меню всё ещё не уверен. Как вариант, можно показывать только половинку фотки пиццы вместо целого круга, но у нас в меню есть прям пиццы-половинки, так что не прокатит, можем запутать пользователей.

Давайте сравним первый подход с финальным результатом:



А теперь сравним «до» и «после» с симуляцией плохого зрения:



Не бойтесь менять интерфейс и контролы. Нет ничего страшного в том, что кто-то увидит другую кнопку или, например, слайдер. И это не смертельно, если кто-то не увидит чего-то или если заголовок будет другой.
А UITabBarController мы не трогали, потому что при большом размере текста он «из коробки» по длинному тапу умеет крупно показывать иконку и заголовок вкладки точно так же, как иос показывает изменение громкости.

Показываем, как это всё устроено внутри


Каждый логический UI-компонент в iOS-приложении Додо Пиццы выделен в отдельный UIViewController. У каждого такого контроллера в отдельный файл выделен UIView. Подробнее об этом можно почитать в наших статьях: 

Контроллер, полегче! Выносим код в UIView
Контроллер-луковка. Разбиваем экраны на части

Вынесение логических UI-компонентов в отдельный UIViewController здоровски упростило задачу по модификации интерфейсов под разные состояния. Мы рекомендуем попробовать такой подход, даже если вы не планируете добавлять поддержку Dynamic Type — так проще рулить состояния экранов: реагировать на изменения авторизации, прав, ролей и так далее.

Так вот. Мы добавляем дополнительную прослойку между таким UI-компонентом и его родительским контейнером. У нас она называется StateViewController.


Контроллер с меню встраивает в себя state-контроллер, а он уже встраивает в себя collection — или button-контроллер.

Этот StateViewController показывает тот или иной UI-компонент в зависимости от ситуации.

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

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

  • Показывать список категорий.
  • Выделять выбранную категорию.
  • Обновлять список категорий.
  • Сообщать, что категория «выбралась».

Чувствуете этот чудесный запах свежих протокольчиков? А, не, это команде мобильного апи доставили пиццу. 5 минут перерывчик.

2 слайса спустя
«… Ну и оборачиваем мы такие наши компоненты для выбора категорий в протоколы, А ОНИ ИМ КАК РАЗ!»
Подсказка: запустите Accessibility Inspector, чтобы легко проверять реагирование интерфейса на смену настроек дайнамик тайпа. Для этого в открытом икскоде нажмите Xcode → Open Developer Tool → Accessibility Inspector, в нём в девайсах выберите симулятор и перейдите на последнюю вкладку

Ещё подсказка: вынесите на айфоне (не на симуляторе) контрол дайнамик тайпа в Контрол Центр, чтобы легко и быстро менять размер текста. Для этого на айфоне зайдите в Settings → Control Centre → Customize Controls и добавьте Text Size.

Обычную выбиралку категории мы обозвали CategoriesCollectionViewController, а для слабовидящих — CategoriesButtonViewController. Общий для них протокол назван CategoriesPickerProtocol. Общий стейт-контроллер — CategoriesStateViewController.

Описываем в нашем CategoriesStateViewController возможные состояния:

private enum State {
    case collection, button
}

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

private var state: State = .collection {
    didSet {
        if state != oldValue {
            updateViewController(for: state)
        }
    }
}

private func updateViewController(for state: State) {
    let viewController = self.viewController(for: state)
    self.updateController(with: viewController)
}

private func viewController(for state: State) {
    switch state {
    case .collection:
        return CategoriesCollectionViewController.instantiateFromStoryboard()
    case .button:
        return CategoriesButtonViewController.instantiateFromStoryboard()
    }
}

instantiateFromStoryboard() — метод из самописного экстеншна на вьюконтроллер, создаёт инстанс контроллера из сториборды, если у них совпадают названия. Код есть в исходниках в конце статьи.

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    super.traitCollectionDidChange(previousTraitCollection)
    self.updateStateToCurrentContentSize()
}

private func updateStateToCurrentContentSize() {
    let contentSize = self.traitCollection.preferredContentSizeCategory
    self.updateState(to: contentSize)
}

private func updateState(to contentSize: UIContentSizeCategory) {
    self.state = contentSize.isAccessibilityCategory ? .button : .collection
}

Описываем протокол CategoriesPickerProtocol, попутно добавляя ещё два протокола: для делегата и для датасурца.

protocol CategoriesPickerProtocol where Self: UIViewController {
    var datasource: CategoriesDatasource? { get set }
    var delegate: CategoriesDelegate? { get set }

    func select(_ category: ProductCategoryModule.ProductCategoryViewModel)
    func updateCategories()

    var selectedCategory: ProductCategoryModule.ProductCategoryViewModel? { get }
}

protocol CategoriesDatasource: class {
    var categories: [ProductCategoryModule.ProductCategoryViewModel] { get }
    func index(of category: Product.ProductCategory) -> Int
}

protocol CategoriesDelegate: class {
    func productCategoriesView(_ categoriesPicker: CategoriesPickerProtocol, didSelect category: ProductCategoryModule.ProductCategoryViewModel)
}

Реализацию показывать особого смысла нет, там просто каждый пикер отображает категории и сообщает наверх об их смене.

Подробный пример использования стейт-контроллеров для дайнамик тайпа можно взять в моём репо на GitHub.

Кстати, мы расширяемся
Теги:
Хабы:
+22
Комментарии 20
Комментарии Комментарии 20

Публикации

Информация

Сайт
dodo.dev
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия