В последнее время ну очень популярна стала такая вещь, как ребрендинг. Одни меняют лого и название. Другие, вдобавок к этому, еще и кардинально меняют цвета.
Популярность эта, однако, не сопровождается должным количеством технических статей, которые мне, как разработчику, наиболее интересны.
Будем это исправлять.
Всем привет! Меня зовут Слава. Я мобильный разработчик. И в этой статье я поделюсь своим уникальным опытом того, как Леруа Мерлен стал Лемана ПРО. Собственно, рассказывать буду про мобильную разработку 🤷🏽.
Спойлер. Уникальность заключается в том, что примерно в одного человека нужно было "отребрендить" и Android, и iOS. Ну и в статье будет рассказываться про обе платформы соответственно тоже.
Чего делать
Давайте определимся, в чем же заключается ребрендинг?
Многим может показаться, что изменились только цвета. По большому счету так и есть, но это далеко не все визуальные изменения. Также нужно было:
Заменить брендовые картинки.
Обновить название.
Актуализировать lottie анимации.
Итого (вместе с цветами) — 4 простых действия.
Глава 1. Цвета
Немного цифр.
У нас есть 2 клиентских приложения. По одному на Android и iOS.
Под каждую платформу мы верстаем несколькими способами. В идеале такого быть не должно, но мы растем и развиваемся, а также переезжаем на более новые технологии.
На момент начала ребрендинга в Android у нас было 145 скринов на Jetpack Compose и 4 скрина на Android View с версткой в .xml
. Последнее — legacy и техдолг.
В iOS же дела обстояли следующим образом:
Storyboard — 33 скрина. Cюда включены все
.storyboard
,.nib
,.xib
файлы и так далее. Legacy, на котором не пишем ничего нового.UIKit кодом — 72 скрина. Не декларатив, конечно, но достаточно просто и надежно.
SwiftUI — 21 скрин. Вечная технология будущего, которая у нас выбрана как ориентир.
Скрытый текст
в Android | в iOS | |
1 | Android View с версткой в | Storyboard |
2 | Android View без верстки в | UIKit кодом |
3 | Jetpack Compose | SwiftUI |
.storyboard
в iOS — это в прямом смысле .xml
файл. Код здесь, однако, куда менее читабелен, чем в Android.
Верстка на UIKit кодом напоминает верстку с ConstraintLayout в Android. Но на деле прямым аналогом будет верстка совсем без .xml
.
Ну а про декларативные SwiftUI и Jetpack Compose вы, наверняка, и так слышите на каждом шагу. Концептуально вещи очень похожие, хоть и есть различия (и порой большие).
К чему все это? Код пишется по-разному и для разных способов верстки попросту несовместим. Точнее, трудно совместим.
Например, в Jetpack Compose используемые из дизайн-системы цвета у нас поддерживают смену светлой-темной темы, а в .xml
— нет. Да, конечно, можно добавить этот функционал и для .xml
, но зачем, если последнее — legacy...
Дизайн-систему я упомянул не просто так. Дело в том, что цвета у нас лежат в библиотеках той самой дизайн-системы. Приложения в идеале должны использовать только эти цвета и никакие больше.
Для чего это нужно? Во-первых, для порядка и консистентности. Во-вторых, если поменяются цвета в дизайн системе, то и по приложению они также поменяются. Но после обновления версии библиотеки, конечно же.
Не уходя далеко хотелось бы познакомить с таким термином, как токен. Токен — это минимальный юнит дизайн-системы, который используют дизайнеры для интеграции стилистики в разработку. Он, как правило, имеет семантическое название. Так, токен может обзываться как цвет для основного текста, md.ref.palette.error40 или же Fronton.color.backgroundPrimary.
А вообще понятие токена очень растяжимое, и каждый трактует его как хочет.
Будь у нас идеальный вариант, задача по смене палитры заняла бы от силы день. Увы, все было не совсем так.
Что по Android части, что по iOS, в отношении цветов имели следующее:
Одни — из дизайн-системы.
Другие — захардкожены.
Третьи — вообще цвета из SDK.
Огромным "подарком" стало еще и то, что все цвета в .storyboard
'ах были захардкожены. Вообще все. А дополнительную сложность накладывало то, что цвета здесь могли описываться по-разному, в зависимости от colorSpace.
Вообще разные colorSpace'ы теоретически можно было встретить и в
.swift
файлах, но у нас такого не было:
UIColor(red: 0.12, green: 0.34, blue: 0.56, alpha: 1.0)
UIColor(white: 0.5, alpha: 1.0)
<color
key = "textColor"
colorSpace="calibratedRGB"
alpha="1"
blue="0.58431372550000005"
green="0.58431372550000005"
red="0.58431372550000005"
/>
<color
key = "backgroundColor"
colorSpace = "custom"
customColorSpace = "genericGamma22GrayColorSpace"
alpha = "1"
white = "1"
/>
Это еще отформатировано, чтобы проще было читать. А так все записывается в одну строку.
Вообще первая идея по перекраске (не моя) заключалась в том, чтобы взять и везде заменить все цвета А на цвета Б. Например, если раньше был #5AB030, а стал #FDC300, то нужно просто средствами IDE заменить все #5AB030 на #FDC300.
Это простейшее решение, но у него есть ряд недостатков:
Лишены всех плюсов хранения цветов только в дизайн-системе.
Добавившиеся или убавившиеся пересечения между токенами необходимо разрешать руками. На скрине ниже пример того, что это такое.
Если цвета поменяются, то (опять) придется обновлять их через замену в IDE. Также из-за этого может повториться предыдущий пункт.
Последние два пункта не являются какими-то вымышленными, и мы фактически с ними столкнулись: и пересечения были, и цвета в процессе корректировались несколько раз.
Недостатки недостатками, но с iOS в самом начале мы так и поступили. В качестве быстрой демки для дизайнеров — круто, но как конечное решение для пользователей — нет. Где-то цвета были не те. Где-то появились странные белые подложки, которых не должно было быть.
В итоге остановились на решении, в котором избавляемся от захардкоженных и системных цветов, привязывая их к дизайн-системе. Делать это изначально предполагалось в ручном режиме. Казалось, что речь идет о паре десятков привязок, но не тут-то было.
И снова к цифрам. Ниже приведена статистика по количеству бесхозных цветов, с которыми предстояло работать:
69 — в
.xml
файлах113 — в
.kt
файлах (Jetpack Compose + Android View)573 — в
.swift
файлах (UIKit кодом + SwiftUI)857 — в
.storybard
файлах и в его товарищах
Сами по себе цифры и так достаточно большие. Неплохо бы еще и понимать то, как происходит привязка цвета. Для этого нужно:
Получить его hex и alpha.
Пойти в Figma дизайн-системы, где лежат все токены с цветами.
Найти токен с идентичным или приближенным цветом (на основе hex и alpha из п.1).
Заиспользовать найденный токен в коде.
Алгоритм вроде бы простой, однако есть нюансы.
Вернемся немного назад. Ранее я уже писал, что цвета в .storyboard
'ах задаются по-разному в зависимости от colorSpace. Из-за этого и hex будет вычисляться по-разному.
Вообще нетривиальное получение hex характерно не только для .storyboard
'ов, но также и для всего остального. Вот несколько примеров:
Color.DarkGray
в Jetpack Compose.
Соответствует #444444, но нужно лезть в SDK.Color.gray
в SwiftUI.
Соответствует #8E8E93, но нужно еще знать, где смотреть.Color.argb(12, 34, 56, 1)
в Android View.
Соответствует #0C2238, но нужно вычислять либо вручную, переводя каждую составляющую из decimal в hexadecimal, либо через Figma.UIColor(red: 0.12, green: 0.34, blue: 0.56, alpha: 1.0)
в UIKit. Соответствует #1F578F, но нужно сначала помножить значения на 255, а потом уже перевести, как в предыдущем пункте.
И даже если мы найдем hex, то одному значению может соответствовать сразу несколько токенов. Что в этом случае делать? Если есть понимание семантического значения — проставлять соответствующий токен. Например, (у нас) для текстов используются в первую очередь цвета из Text категории, но никак не из Background. Если же непонятно, что проставлять, нужно идти к дизайнерам.
Цифры по количеству бесхозных цветов и описание нюанса здесь присутствуют исключительно с одной целью: показать, что привязка руками потребует очень много времени и сил.
Осознавая данный факт, я в свое время пришел к тому, что нужно где-то хитрить.
Были написаны скрипты, которые предоставляли аналитическую информацию: сканировали файлы проекта, по regex находили бесхозные цвета и выводили файл, номер строки, hex и alpha. Всего 4 скрипта, под каждый тип файлов: .storyboard
, .swift
, .kt
и .xml
. У каждого скрипта свои правила перевода в hex и alpha.
Пример работы скрипта:
LMFieldView.xib:51 / [color=#333333, alpha=1.00]
LMFieldView.xib:64 / [color=#F0F1F2, alpha=1.00]
LMFieldView.xib:70 / [color=#000000, alpha=0.00]
LMFieldView.xib:96 / [color=#FFFFFF, alpha=1.00]
FeedbackCheckLicenseTableViewCell.xib:26 / [color=#B7B7B7, alpha=1.00]
FeedbackCheckLicenseTableViewCell.xib:33 / [color=#53C43F, alpha=1.00]
FeedbackHeaderPhotosTableViewCell.xib:27 / [color=#000000, alpha=1.00]
FeedbackHeaderPhotosTableViewCell.xib:31 / [color=#000000, alpha=0.00]
FeedbackHeaderPhotosTableViewCell.xib:49 / [color=#959595, alpha=1.00]
FeedbackMultiTextTableViewCell.xib:27 / [color=#000000, alpha=0.00]
FeedbackPhotosTableViewCell.xib:34 / [color=#000000, alpha=0.00]
FeedbackSendButtonTableViewCell.xib:22 / [color=#53C43F, alpha=1.00]
А вот как выглядела LMFieldView на 51й строке (отформатировано, расписано в несколько строк для наглядности):
<color
key="textColor"
red="0.20000000000000001"
green="0.20000000000000001"
blue="0.20000000000000001"
alpha="1"
colorSpace="calibratedRGB"
/>
Стоит отметить, что скрипт также выводил цвета из дизайн-системы, чтобы было проще искать совпадения.
Вся эта аналитическая информация позволила уже далее руками привязать большую часть цветов. Где-то за неделю так получилось сделать с .kt
, .xml
и .swift
файлами. В итоге во всех этих файлах абсолютно все цвета ссылались на цвета из библиотек дизайн-системы.
Оставались .storyboard
'ы. К ним (наверное, и из-за сложившейся нелюбви) мне довелось подобраться в последнюю очередь.
Что здесь было не так? Оказалось, что нельзя где-то вовне объявить цвет и ссылаться на него из .storyboard
'а. Такого механизма попросту нет (либо плохо искал). Цвет всегда будет храниться в каждом файле индивидуально.
Казалось, что здесь можно только поменять одни захардкоженные значения на другие. Оказалось, однако, что это не совсем так. Есть приемлемая альтернатива — Named colors:
<capability name="Named colors" minToolsVersion="9.0"/>
Данный функционал позволяет обзывать цвет и использовать его как ссылку в рамках одного файла. То есть мы можем сделать формальную привязку:
Определить hex и alpha заменяемого цвета (это мы уже умеем на основе скрипта).
Взять название цвета в дизайн-системе.
В файл добавить именованный цвет, имя и значения которого задаются на основе дизайн-системы.
Но фактической привязки у нас так и не состоялось. Если обновятся цвета в библиотеке дизайн-системы, то эти изменения никак не подтянутся в .storyboard
файлы. По этой причине появился еще один скрипт, который гулял по подобным файлам, находил именованные цвета и, если они соответствовали дизайн-системе, обновлял их значения.
У нас используется Cocoapods. Можно было бы закрепить выполнение последнего скрипта за фазой post_install и вообще забыть про его существование.
Итак, все готово к тому, чтобы брать и менять цвета в .storyboard
'ах руками.
Привязки легко производятся через отдельную интерактивную панель. Через код же сделать то же самое уже проблематично, так как изменения будут происходить в разных частях.
Для того, чтобы цвета из дизайн-системы отображались в выпадающем списке, пришлось добавить
.colorset
'ы всех цветов в.xcassets
библиотеки.
Проблема интерактивной панели в том, что такие замены занимают много времени. Даже если условно на 1 замену тратить по 3 минуты (а это приуменьшенное время), то потребуется 43 часа на рутинную работу по 857 кейсам. А еще стоит учесть, что некоторые кейсы занимали и по 15 минут. Это связано с тем, что иногда не совсем понятно, на какой View нужно поменять цвет, так как необходимо сопоставлять нечитабельный код с визуализацией. Более того, даже найдя View неплохо бы еще и найти характеристику, у которой меняется тот самый цвет. А характеристика, в свою очередь, могла тоже лежать на какой-то другой вкладке интерактивной панели.
В итоге скрипт для анализа .storyboard
'ов научился не только выдавать информацию, но еще и подменять цвета. Причем местами хитрым способом:
Для текстов выбирал сначала цвета из Text категории.
Цвета Pressed категории игнорировались при наличии токенов с идентичным значением.
Hex и alpha могли отличаться от тех, которые в дизайн-системе, основываясь на небольшую допустимую погрешность.
Итог:
Все цвета привязаны.
Обновление цветов по приложению сводится к обновлению значений токенов в библиотеках дизайн-системы.
Есть скрипты, проверяющие код на предмет привязок из п.1. Их можно переиспользовать в качестве линтеров.
Теперь потенциальная задача по перекраске приложения с зеленого, скажем, в оранжевый, займет порядка часа, а не месяца-двух. Как финал главы, перекрашиваем приложение за 2 минуты.
Глава 2. Картинки
Что в Android, что в iOS разработке (да и, наверное, в любой другой) все картинки можно разбить на две категории:
Те, которые подлежат покраске (он же tint). Такие штуки называю иконками.
Те, которые ни в коем случае нельзя красить. Представителей этой категории называю иллюстрациями.
Первые, на мой взгляд, должны всегда явно краситься в какой-то конкретный цвет. Таким образом, любая картинка либо всегда красится (и является иконкой), либо всегда не красится (и является иллюстрацией).
Иконки
Иконки у нас хранятся в библиотеках дизайн-системы.
Однако они используются не везде. Где-то используются legacy иконки, лежащие локально в проекте. Причем есть здесь несколько дополнительных проблем:
Такие иконки могут содержать дубликаты. Дубликаты эти могут либо незначительно отличаться, либо не отличаться вовсе.
Какие-то иконки уже содержат конкретный цвет и не красятся в дальнейшем (что, на мой взгляд, неверно).
Иконки могут отличаться в размерах.
Подобно цветам работа здесь заключалась в синхронизации с библиотеками дизайн-системы: нужно было избавиться от локальных иконок. Какие-то скрипты здесь показались избыточными, поэтому все было сделано руками.
Был, правда, один баг с .storyboard
'ами. Иконки из библиотеки нормально отображались в интерактивной панели. Смена иконки также отрабатывала на ура и меняла визуальный результат.
Тем не менее при запуске приложения иконки не показывались. В лог сыпала ошибка c текстом:
Could not load the "arrows__chevron_right__basic" image referenced from a nib in the bundle with identifier "ru.leroymerlin.ios"
Фикс: скрипт, который в post_install фазу Cocoapods копирует .xcassets
из библиотеки в проект.
Иллюстрации
Иллюстрации у нас не хранятся в дизайн-системе, поэтому алгоритм работы будет несколько другим:
Избавляемся от идентичных дублей.
Собираем все иллюстрации по приложениям.
Отправляем собранные иллюстрации дизайнерам на обновление.
Получаем новые иллюстрации.
Заменяем.
Готово.
С иллюстрациями и дублями был один интересный момент. Были такие, которые визуально выглядели по-разному, стилистически могли сильно отличаться, но служили для одной и той же миссии. Это то, что осталось как часть legacy.
Сейчас же, после ребрендинга, используется единый новый стиль для иллюстраций. Подобных дублей (хоть это и не совсем так) больше нет.
Стоит отметить, что к единообразию пришли и куда более значимые картинки: пины для карт. В приложении 3 места, где используются карты. В каждом из таких мест ранее использовались свои незначительно отличающиеся пины.
С дублями разобрались. Дальше нужно все собрать и выдать в дизайн на корректировку.
Выдать иллюстрации — это, конечно, хорошо. Но как насчет того, чтобы их можно было открыть (хотя бы для просмотра нужных размеров)? Часть выдаваемых векторок была в .pdf
и .xml
форматах, которые та же Figma просто так скушать не могла. На помощь пришли они:
PDF to Figma. Плагин Figma для вставки
.pdf
.VectorDrawable to SVG. Сервис для конвертации Android
.xml
векторок в.svg
.
Кроме векторных иллюстраций может потребоваться еще и выдать растровые. Последние, что на Android, что на iOS, образуют пак из нескольких идентичных картинок в разных размерах. Дизайну нужны файлы в размере x1. Но что есть файл в размере х1?
В iOS иллюстрации, в рамках пака, находятся в одной папке. Множители прописываются здесь же в Contents.json
. На крайний случай там можно найти нужный файл. Но, как правило, это не требуется, так как названия картинок будут отличаться, а картинка в размере x1 будет иметь какой-нибудь суффикс типа @1, x1, -1 или не иметь суффикса вовсе.
{
"images" : [
{
"filename" : "pro-card.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "pro-card-x2.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "pro-card-x3.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
В Android же иллюстрации разбрасываются по разным общим размерным папкам (drawable/hdpi, drawable/xhdpi и так далее), образуя что-то вроде "размерных маек". Здесь нужно помнить, что множителю x1 соответствует папка mdpi и именно оттуда нужно забирать и выдавать картинку на дизайн.
Эта часть, про растр, приводится в статье исключительно как памятка. В свое время я выдал картинки из Android в hdpi, а потом пришлось перезапрашивать.
Глава 3. Название
Одна из самых быстрых задач — поменять название.
Через IDE это делается ну очень быстро и очень приятно. Единственное, после такой замены нужно было откатить текст, используемый в качестве названия для самого launcher'a приложения.
И здесь не обошлось без приключений, пусть и небольших.
У нас очень недолго был баг с двойным названием. Суть была в том, что с сервера нам прилетал текст, содержащий имя компании в названии магазина. На клиенте эта часть затиралась. Получалась новая строка, без имени компании, которую использовали далее под свои нужды. Так Леруа Мерлен Выхино превращалось в Выхино.
Повсеместная замена имени бренда через IDE, к сожалению, "поправила" и магию, описанную выше. И из-за этого часть Леруа Мерлен не вырезалась. Более того, еще и добавлялось новое название...
Стоит отметить, что была еще и схожая проблема с deeplink'ами, в том плане, что логика с названием лежала где-то глубоко на клиенте.
Глава 4. Lottie анимации
Анимации во время ребрендинга нужно обновлять хотя бы по той причине, что они могут содержать в себе старый брендинг (цвета, названия, логотипы).
Здесь алгоритм максимально схож с кейсом с иллюстрациями: собираем, отдаем, получаем, обновляем.
Как и вообще со всем остальным, здесь пришлось поработать напильником. Дело в том, что от QA прилетел баг, что новая анимация работает криво.
На скрине представлены примеры новой и старой анимации. Выглядит синхронно, но есть нюанс. Дело в том, что почему-то "поехало" количество фреймов в анимации.
Решение: в коде смещаем критические фреймы в соответствии с новыми анимациями.
Более правильное решение: перезапрашиваем анимации.
За кулисами
Фактически на этом по задачам все. Однако были некоторые интересные моменты, о которых я бы и хотел еще немного поведать.
Компоненты не из дизайн системы
Серьезная проблема заключается в том, что в приложении используются компоненты не из дизайн системы. По дизайну в Figma используется компонент из дизайн системы, но в разработке все иначе. Формально проблему можно разбить на две:
Реализация компонента есть в библиотеке, но почему-то разработка ее не использует.
Реализации компонента нет в библиотеке и разработка имплементирует компонент в каждом месте индивидуально как хочет.
Первая проблема решается переездом на компонент из дизайн системы. Но само наличие такой проблемы должно насторожить и заставить задуматься о создании линтеров. Например, кейсом с такой проблемой в Android было использование material.Button
, а не FrontonButton
. Значит, пишем линтер, который на корню блокирует использование material.Button
. И так можно поступить вообще со всеми компонентами. А можно поступить еще проще и в текущем примере выпилить зависимость на Material, однако это не всегда возможно.
Когда реализация компонента в библиотеке отсутствует, хорошо бы не плодить одно и то же, а создать промежуточное решение и использовать его. Это полезно еще и тем, что если в будущем появится компонент, то будет проще на него переезжать.
Организационные моменты
Разработка велась под секретом. И от других разработчиков в том числе.
Пушить изменения было нельзя. В таких условиях QA не могли переключиться на какую-то ветку и что-то там потестировать. Приходилось скидывать архивы с исходниками. Причем в архиве были исходники не только проекта, но еще и библиотек дизайн-системы.
Из-за этой же секретности по готовности нельзя было просто взять и влить изменения в рабочую ветку. Нужна была отмашка по релизу. Пришлось достаточно долго поддерживать жизнеспособность данного творения локально. Чем больше проходило времени — тем больше конфликтов появлялось. А эти конфликты порой решались небыстро.
Старые скрипты — новые линтеры
Ранее я рассказывал про скрипты, предоставляющие аналитическую информацию. Ранее они были нужны для упрощения работы, а после этой самой работы у них появилась вторая жизнь. Они стали линтерами, запрещающими разработке использовать цвета не из дизайн-системы.
Скрипты эти были написаны на Kotlin. В идеале их надо было писать на том же Python, чтобы потом спокойно можно было встроить в CI/CD. Я же в свое время не думал, что они потом когда-то пригодятся. Если (вдруг) вы столкнетесь с подобным кейсом, желание запускать с терминала Kotlin скрипты никуда не уйдет, а переписывать все это добро вы не хотите, то выход есть:
kotlinc myScript.kt -include-runtime -d myJar.jar
java -jar myJar.jar
На этом точно все! Спасибо за прочтение!