Что такое Angstrom Style System?
В обычном мобильном приложении интерфейс — одна из самых сложных и самых важных частей. То, что видят пользователи, что оценивают в первую очередь заказчики, при приёме работы.
Даже если дизайн рисуется до разработки, в процессе создания первой версии всё-равно появляется огромное количество изменений. Тут кнопочку перекрасить, тут подвинуть, там шрифт увеличить/уменьшить, потому что не поместилось и так далее. Каждое из этих изменений требует доработки. Часто таких доработок бывает несколько сотен или даже тысяч, если не получается сразу подобрать правильные параметры.
В такой ситуации очень выгодным получается отделить настройки дизайна от самого интерфейса и настраивать независимо. Эту задачу должна решать система стилей, идеологически — как CSS для HTML.
В статье я покажу, как именно я стараюсь организовать стили в приложении, и какими средствами я для этого пользуюсь.
Структура стилевой системы в приложении
Когда я разрабатываю приложение, обычно получается два слоя стилевых настроек. Первый слой содержит самое базовое:
- цвета приложения,
- шрифты приложения,
- базовые стили текстов приложения,
- базовые параметры. Например, общий радиус скругления углов, или какие-то шаги отступов для сетки.
Это те параметры, которыми пронизано всё приложение. Если вдруг дизайнер решил, что он выбрал неудачный шрифт и его требуется поменять, то поменять нужно сразу везде. Этот слой обычно достаточно небольшой и понятный всем. Можно отдать поредактировать/понастраивать его дизайнерам или даже, в особо доверенных случаях, заказчику.
Из этих параметров в огромном количестве вариаций создаётся второй слой стилей. Тут уже описываются конкретные экраны, конкретные компоненты интерфейса. Например.
- лейбл-раз: шрифт номер два, цвет красный-три, выравнивание влево,
- кнопка-два: шрифт пять, цвет синий-активный, выравнивание по центру.
Файлы второго слоя обычно объемные, их удобно разбить на несколько, по экранам или разделам приложения.
Достоинства стилей в приложении
Стили, особенно организованные в два слоя, очень хорошо отделяют настройки интерфейса от кода. Проще найти в нескольких стилевых файлах, где настройка вот этого размера шрифта, чем искать по сотням исходников или огромным сторибордам.
В случае, если научиться загружать стили из удаленного источника (из файла в ДропБоксе), то в сотни раз ускоряется настройка дизайна при удалённой разработке. Одно дело, когда дизайнер может сесть вместе с разработчиком и попытаться быстро поднастроить какой-то параметр, а когда это невозможно? Каждая итерация может занимать часы и дни. В данном же случае обновил файл, перезапустил приложение (или сделал хитрый секретный жест), посмотрел результат.
В некоторых ситуациях удобно использовать скины. Для читалок книжек это, например, необходимость, читать днём удобно со светлым фоном, ночью — с тёмным. Иногда этого хочет заказчик, иногда по дизайну требуется для «вау-эффекта». Если сделать два варианта первого слоя стилей, то можно их переключать, оставляя второй слой одним и тем же. В результате скины получатся почти бесплатно.
Когда появилась необходимость в системе стилей?
Необходимость в системе стилей появилась сразу с двух сторон:
- в аутсорсинговой компании разрабатывается множество приложений. Приятно иметь общую схему их работы, чтобы один разработчик мог поправить код другого. И заказчики постоянно меняющие требования — тоже встречаются чаще, чем хотелось бы.
- в разработке своих приложений хочется всегда сделать «идеально». Когда мы разрабатывали Ангстрем с Ильёй Бирманом, требовался механизм настройки дизайна, который бы позволил Илье играться с настройками сложных компонентов, таких, как кастомный курсор.
Я предпочитаю тестировать новые идеи на своих собственных проектах, а не на проектах заказчика, поэтому Ангстрему повезло, и я создал для него систему стилей. В ней для описания стилей использовался файл на JSON, в который я добавил возможностей включения других файлов (в том числе «из Дропбокса»). Во фреймворке также были реализованы колбеки, которые сообщали, что стиль изменился. Я подключил обновление стилей на тряску Айфона, получилось занятно. Поправил файл в Саблайме, сохранил, трясанул Айфон, стили подгрузились и применились ко всему приложению.
Недостатки существующей системы, что «не взлетело»?
В целом идея оказалась настолько удачная, что она быстро распространилась на все наши создаваемые приложения. В некоторых случаях это позволило сильно сократить время разработки (когда дизайнер заказчика, к примеру, хотел поднастроить шрифты или цвета), иногда дизайн с момента разработки до релиза менялся целиком несколько раз, практически полностью без участия разработчика (или с минимальным участием, чтобы настроить тонкие моменты).
Но несколько моментов мне не понравились.
Во-первых, JSON. Постоянно получалось, что случайно забыл запятую или сделал другую глупую ошибку в файле, и ломались все стили. JSON не проверяет названия пропертей (это просто строки и ему всё-равно, что там написано), типизация введена искусственно, что тоже мешает раннему нахождению ошибок, и так далее.
Тот же JSON заставил меня кодировать типы данных прямо в названиях. Цвета у меня заканчивались на color, точки — на center или point, и так далее. В результате названия полей получались сильно длинее, чем требуется.
После первых попыток использования стилевой системы, выяснилось, что самый удобный вариант — когда по JSON'у создаётся иерархия классов, полностью повторяющая иерархию объектов в JSON'е. В приложении тогда появляется строго-типизированная структура, к которой можно удобно обращаться как к обычному коду. В принципе, можно было привязать к обновлению стилей UI-компоненты, чтобы стиль знал, что он применяется вот к этой кнопке, и если вдруг обновился (скачался новый стиль из Дропбокса), то сам бы кнопку и обновил. Но на деле оказалось, что такая магия только мешает жить, лучше прописывать правила обновления явно.
В-третьих, я не создал консольную утилиту, которая бы генерила классы стилей. А сами стили пересоздавались при запуске приложения в симуляторе. Это оказалось существенным минусом, как при подключении стилей в проект, так и при последующем их обновлении.
Также немного мешал Objective-C и необходимость поддержки старых версий iOS. Код сгенерированного класса получался большой и неприятный, очень хотелось бы его оптимизировать.
S2 — более простая и эффективная система стилей
Поняв недостатки, я попробовал улучшить систему с тем, чтобы она лучше подходила для решаемых задач, по всем параметрам:
- Теперь формат файла — KTV. Он поддерживает и ссылки, и миксины, и типы, включая базовый тип color. KTV умеет без изменений читать и JSON-файлы, поэтому мои старые стили перекочевали в новую структуру без изменений.
- Файл стиля создается из ktv-файла на Swift. При этом, если не использовать поддержку Objective-C (которая тоже возможна), то получается компактный, удобный как для просмотра так и для использования класс (иерархия вложенных классов), не засоряющих глобальное пространство имён приложения.
- Благодаря поддержке типов в KTV, стало возможным отказаться от суффиксов в названиях, что упростило имена пропертей и классов.
- Появилась консольная утилита, которая может по ktv-файлу (или нескольким файлам) создать классы стиля.
Для примера приведу (очень небольшой) фрагмент исходного KTV:
{
maxWidthForIPad: 600
darkTheme: false
defaultFontName: HelveticaNeue-Light
bolderFontName: HelveticaNeue
boldFontName: HelveticaNeue-Medium
defaultSymbolFontName: AngstromSymbols-Light
bolderSymbolFontName: AngstromSymbols
basicColors: {
plateBackground: #edf5f4
separators: #00407020
}
ilya: {
aboutBackground: @basicColors.plateBackground
listSeparator: @basicColors.separators
}
colors: {
about: {
background: @ilya.aboutBackgroundColor
separator: @ilya.listSeparatorColor
}
}
about: {
margins: [0, 0, 0, 0]
separatorSpacing: 10
background: @colors.about.backgroundColor
separator: @colors.about.separatorColor
}
}
И соответствующий ему фрагмент стилевого файла, который получается:
let S2 = CONStyle()
public struct CONStyle: S2Object {
private static let _rootStyle = S2
public struct BasicColors: S2Object {
let separators = UIColor(colorLiteralRed:Float(1.0), green:Float(1.0), blue:Float(1.0), alpha:Float(0.0))
let plateBackground = UIColor(colorLiteralRed:Float(0.929411764705882), green:Float(0.929411764705882), blue:Float(0.929411764705882), alpha:Float(1.0))
}
let basicColors = BasicColors()
public struct Ilya: S2Object {
let aboutBackground = _rootStyle.basicColors.plateBackground
let listSeparator = _rootStyle.basicColors.separators
}
let ilya = Ilya()
public struct Colors: S2Object {
public struct About: S2Object {
let background = _rootStyle.ilya.aboutBackgroundColor
let separator = _rootStyle.ilya.listSeparatorColor
}
let about = About()
}
public struct About: S2Object {
let margins = UIEdgeInsets(top:0.0, left:0.0, bottom:0.0, right:0.0)
let separatorSpacing = Int(10)
let background = _rootStyle.colors.about.backgroundColor
let separator = _rootStyle.colors.about.separatorColor
}
}
Конечно же, использование Swift будет неудобно для полностью Objective-C проектов. Это потащит за собой в проект Swift-рантайм, который может существенно увеличить размер приложения. Но, во-первых, не сильно сложно написать генератор чисто Objective-C кода, а во-вторых, всё сейчас идёт к массовому использованию Swift в приложениях, так что и генерируемый S2 код будет «в тему».
vintage
Думаю тут стоило идти по пути совместимости с CSS, а не JSON.
bealex
Про CSS я думал. Получилось, что они очень плохо ложатся на UI мобильных приложений. Впрочем, попытки применения CSS к ним уже были (например, https://nativecss.com).
vintage
А в чём сложности?
bealex
Никаких сложностей, просто, по моему мнению, CSS не подходит для нативных UI мобильных устройств, чем я занимаюсь.
vintage
Почему это?
bealex
Короткий ответ — потому что он сделан для другого. Каскадность совершенно ни к чему, нет, как таковой, иерархии блоков, нет DOM'а, чтобы стили привязывать к элементам, лейаут делается совершенно по иным правилам. Это только те причины, которые на поверхности, реализовывать CSS для нативного использования тоже не очень приятно.
vintage
Не путайте CSS как язык, и CSS как синтаксис. В чём проблема взять синтаксис и часть языковых средств (определения цветов, отступов), выкинув ненужное (каскад, стандартные раскладки) и добавив недостающее (например, вложенные определения из less/scss/stylus)?
bealex
Извините, я никак понять не могу. Вы в принципе не признаёте иных решений, кроме тех, которые знаете, или просто желаете набросить?
Если вам требуется CSS, используйте конечно. Я ведь не заставляю использовать свою систему. Для этого и существуют разные варианты, чтобы иметь возможность выбрать нужный в конкретном случае. Впрочем, можно всегда пользоваться только микроскопом, это тоже валидное решение.
vintage
Вы зря воспринимаете критику в штыки.
bealex
В ваших комментариях нет критики :-)