- PVSM.RU - https://www.pvsm.ru -
Если вы разрабатываете продукт для масс-маркета, то вероятнее всего им пользуются люди с плохим зрением. Если вы стремитесь делать удобные интерфейсы, то надо сделать удобно для всех клиентов, в том числе для людей с плохим зрением. Думаю, мы часто забываем об этом. И это пора исправлять.
Я ввёл в поиске в App Store запрос «доставка пиццы», скачал первые 24 приложения и проверил, кто из них предоставляет интерфейс для людей с плохим зрением.
2 из 24. Причем один из двух, кажется, сделал это случайно: при увеличении размера шрифта весь интерфейс «плывёт» и им становится пользоваться только сложнее. Печально.
iOS-приложением Додо Пиццы ежемесячно пользуются 550 000 человек. Даже если у 1% наших пользователей включен увеличенный шрифт, то это 5500 человек, которым некомфортно пользоваться нашим приложением. Будем исправлять.
adjustsFontForContentSizeCategory
.traitCollectionDidChange
.Откатываемся назад и начинаем думать, как всё сделать хорошо.
Сейчас под картинкой пиццы много пустого места. Попробуем поставить картинку над названием: так она станет больше, а пустое место пропадёт. Для этого мы завернём в UIStackView
картинку и вью-контейнер со всем остальным, а затем будем переключать направление стаквьюхи при необходимости.
У нас нет сепараторов между позициями меню, из-за чего при большом кегле ячейки начинают «слипаться» и ценник пиццы оказывается слишком близко к картинке следующей пиццы. Попробуем добавить сепаратор.
Не то. Во-первых, выглядит так себе. Во-вторых, его будет плохо видно людям с плохим зрением. Даже если перекрасить из серого в чёрный.
Убираем его обратно и пробуем просто увеличить инсет между ячейками.
Вот теперь то.
Промежуточный итог: используем больше места, глаза меньше прыгают со строки на строку, читать стало легче.
Теперь можно увеличить ширину кнопки добавления в корзину, а то она уплыла влево и находится не под пальцем, хотя справа много пустого места. Можно, конечно, и просто передвинуть в правый бок, но тогда будет неудобно левшам и вообще, давайте лучше жестить, чем недожимать. И ещё обводку ей сделаем пожирнее, а то сейчас она не согласована со шрифтом.
Смотрю на всё это и понимаю, что фотка пиццы, конечно, совсем огромная получается. Давайте попробуем её спрятать, может быть и без фоток можно жить.
В целом, меню без фоток не особо потеряло в информативности, зато теперь одна позиция меню почти всегда влазит в экран айфона 6S. Но стало менее привлекательно, СЛЮНКИ НЕ ТЕКУТ ПРИ СКРОЛЛЕ. Такое. Пока что оставим так, хорошенько подумаем и, может быть, попозже всё же вернём картинку.
Теперь категории. В целом, ещё при первом подходе получилось сносно. Наворачиваем по новой.
Попробовал полистать меню и попереключаться между категориями. Всё же нет, получилось плохо: в действии всё рассыпается. У нас при скролле меню автоматически переключаются категории, а на таких больших кеглях это привлекает слишком много внимания.
Давайте заменим UICollectionView
на кнопку, которая будет вызывать UIActionSheet
.
Вооот. Теперь можно взяться за верхнюю панельку, где город, акции, адрес и промокод.
Сначала возьмёмся за выбиралку города. Со шрифтом в кнопке ничего сложного нет, а вот научить «треугольничек» расти вместе со шрифтом — интересно. В нашем случае треугольничек был сделан иконкой в кнопке, которая передвинута на правую сторону через CGAffineTransform
. Ещё как вариант — собирать NSAttributedString
из текста и иконки треугольничка, а потом всё это скормить кнопке. Чтобы иконка нормально скейлилась можно использовать векторную картинку, которая должна обязательно лежать в ассетах с галочкой Preserve Vector Data.
Иконка треугольничка у нас чёрная, а раскрашивается в белый цвет через код. И почему-то при стандартном размере текста на ней вылезают артефакты в виде черных бордеров. Забавно. Не очень. Вылечил, положив в ассеты иконку изначально белого цвета.
Теперь растягиваем додо-рубли, тут всё просто:
А вот теперь вопрос: что будет, если название города окажется длинным и у нас будет много додо-рублей? По идее, нужно сократить название города. Помните, что я говорил о втором варианте добавления такой иконки в кнопку, через NSAttributedString
? Я попробовал и теперь возникла проблема, что при сокращении заголовка у нас и иконка треугольника пропадает, ведь она теперь часть заголовка. Штош. Придётся возвращать логику передвигания иконки через трансформы.
Если вы знаете удобный способ как передвинуть иконку в кнопке на правую сторону и скейлить её вместе со шрифтом в заголовке — скиньте в комменты, пожалуйста.
Наконец-то акции. Тут надо сесть и подумать. Заголовок может быть длинный и даже сейчас он иногда не влазит в одну строку. На большом кегле он не влезет ну вообще никак. Если сделать верхнюю оранжевую панель резиновой и позволить заголовку акции в большом кегле занимать несколько строк, то верхний блок отъест половину экрана даже на больших айфонах, а про 4S вообще можно будет не вспоминать. Это не дело. Можно поиграть с лейаутом внутри ячейки акции: сделать картинку квадратной, а освободившееся место занять заголовком. Но картинки для акций подгоняются под конкретный формат и будут некорректно показываться в другом. Так нельзя.
Сложна.
Так, а можно ж опять полностью убрать картинки и всё место занять заголовком.
Ага, оно. Руки чешутся раскрасить фон под заголовком акции, но это плохо скажется на читаемости. А мы, вроде как, улучшить её пытаемся. Так что ничего не красим и идём дальше, к оставшимся двум кнопкам про адрес и промокоды.
Заголовки в этих кнопках — несокращаемые. Но если их не сокращать, то кнопки наползут друг на друга. И да, спрятать эти кнопки нельзя.
Когда я переделывал акции, не хотел увеличивать высоту верхней оранжевой панельки. Кажется, всё-таки придётся. Хорошо, что не увеличили её в тот раз, а то сейчас бы вообще адуха была. В общем, я собираюсь выделить по одной строчке для каждой кнопки.
Уффф, всё. Насчёт выключенных фотографий в меню всё ещё не уверен. Как вариант, можно показывать только половинку фотки пиццы вместо целого круга, но у нас в меню есть прям пиццы-половинки, так что не прокатит, можем запутать пользователей.
Давайте сравним первый подход с финальным результатом:
А теперь сравним «до» и «после» с симуляцией плохого зрения:
Не бойтесь менять интерфейс и контролы. Нет ничего страшного в том, что кто-то увидит другую кнопку или, например, слайдер. И это не смертельно, если кто-то не увидит чего-то или если заголовок будет другой.
А
UITabBarController
мы не трогали, потому что при большом размере текста он «из коробки» по длинному тапу умеет крупно показывать иконку и заголовок вкладки точно так же, как иос показывает изменение громкости.
Каждый логический UI-компонент в iOS-приложении Додо Пиццы выделен в отдельный UIViewController
. У каждого такого контроллера в отдельный файл выделен UIView
. Подробнее об этом можно почитать в наших статьях:
Контроллер, полегче! Выносим код в UIView [15]
Контроллер-луковка. Разбиваем экраны на части [16]
Вынесение логических 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 [17].
→ Кстати, мы расширяемся [18]
Автор: Алексей Берёзка
Источник [19]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/interfejsy/319085
Ссылки в тексте:
[1] Image: https://habrastorage.org/webt/b3/_r/5q/b3_r5qn5g9o6l-iymufypubsaxe.png
[2] Image: https://habrastorage.org/webt/pg/ff/7t/pgff7tlt4payrwjdasneuk1r8xs.png
[3] Image: https://habrastorage.org/webt/fd/5s/pd/fd5spdixzxaoy0qxmpyhwjmt0tm.png
[4] Image: https://habrastorage.org/webt/ac/gq/1q/acgq1qyz2l_ugejzroyclrtq0k4.png
[5] Image: https://habrastorage.org/webt/-p/gm/dw/-pgmdwq9ek4wgrh501kigthodxw.png
[6] Image: https://habrastorage.org/webt/21/1x/ip/211xip3fvl9-mqmtewbwhyi3y-o.png
[7] Image: https://habrastorage.org/webt/ea/3r/v8/ea3rv8gyeko6dzt_2dkca66d5k8.png
[8] Image: https://habrastorage.org/webt/iw/lp/1b/iwlp1bjobrc6wbshl8gns9eff-c.png
[9] Image: https://habrastorage.org/webt/r5/7k/fz/r57kfzbng-bwybworqlvbzunagk.png
[10] Image: https://habrastorage.org/webt/4i/zk/fg/4izkfgt_ui5yyddl8dj1gzjmqwy.png
[11] Image: https://habrastorage.org/webt/xq/qr/rq/xqqrrq0gwixos4bzfsuay8tfnlk.png
[12] Image: https://habrastorage.org/webt/ek/tu/wj/ektuwjvjhb2dyql9robn3oirlr4.png
[13] Image: https://habrastorage.org/webt/xd/bl/5n/xdbl5nafuxuxirbxb7drudjsnua.png
[14] Image: https://habrastorage.org/webt/xi/_a/g-/xi_ag-vc05u8jfhwvzpi9smxuhg.png
[15] Контроллер, полегче! Выносим код в UIView: https://habr.com/ru/company/dodopizzaio/blog/432718/
[16] Контроллер-луковка. Разбиваем экраны на части: https://habr.com/ru/company/dodopizzaio/blog/434470/
[17] репо на GitHub: https://github.com/AllDmeat/DynamicType-StateViewController
[18] Кстати, мы расширяемся: https://habr.com/ru/company/dodopizzaio/blog/439844/
[19] Источник: https://habr.com/ru/post/452226/?utm_source=habrahabr&utm_medium=rss&utm_campaign=452226
Нажмите здесь для печати.