Хабрахабр

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

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


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

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

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

Добавляем поддержку 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
Контроллер-луковка. Контроллер, полегче! Разбиваем экраны на части

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

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


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

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

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

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

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

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

2 слайса спустя
«… Ну и оборачиваем мы такие наши компоненты для выбора категорий в протоколы, А ОНИ ИМ КАК РАЗ!»

Для этого в открытом икскоде нажмите Xcode → Open Developer Tool → Accessibility Inspector, в нём в девайсах выберите симулятор и перейдите на последнюю вкладку Подсказка: запустите 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 }
} 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.

→ Кстати, мы расширяемся

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть