Создание мобильного приложение — это итеративный процесс. Дизайн начинается в Фотошопе, а разработка — в АппКоде. Но когда первый прототип готов, дизайн и программирование объединяются: для практически любого изменения в интерфейсе необходимо изменить код. Для изменения цвета, шрифта или настройки параметров анимаций, дизайнер просит разработчика о помощи. Это взаимодействие замедляет процесс, а некоторые эксперименты из-за него получаются слишком затратными.
Когда мы с Ильёй начинали делать Ангстрем, то сразу решили создать гибкую стилевую систему, чтобы упростить подстройку дизайна в процессе разработки.
Базовые принципы
Стилевые файлы хранятся отдельно от кода, они должны легко читаться, редактироваться в любом редакторе и парситься приложением. Поэтому в качестве формата я выбрал JSON. Plist или CSS нам подошел меньше. Для хранения стилей мы используем DropBox.
Обновление стилей "на лету", в процессе работы приложения, без перекомпиляции, переустановки или рестарта. Если вы трясанете Айфон, стили обновятся с сервера.
Быстрый запуск приложения вне зависимости от количества стилей. Сейчас в Ангстреме несколько сотен правил, более крупные приложения могут содержать тысячи. Система запаковывает стили в бинарные файлы, которые очень быстро открываются.
Код должен легко поддерживаться. Система позволяет не обращаться к стилям по ключам, как, например, [styler boolForKey:@"isTopBarHidden"]
, так как ошибку в такой строке тяжело искать. Создается объект с соответствующими полями, после чего компилятор его проверяет (style.isTopBarHidden
) и разработчик может использовать этот объект для получения стилей.
Архитектура
Три основных объекта системы стилей:
- Стайлер загружает файлы стилей, рассылает уведомления, если стили поменялись.
- Стилевые объекты создаются Стайлером из файлов стилей, в них лежат значения.
- Подписчики на изменение стилей получают уведомления в случае, когда стили изменяются.
После того, как стайлер создаст стилевые объекты из JSON'а, он:
- Использует стандартный NSArchiver, чтобы сохранить и восстановить кэш стиля в бинарной форме (для быстрой загрузки);
- Позволяет получать значения стилей максимально быстро и удобно, обращаясь к полям класса (для быстрой работы приложения и удобства).
Пример. Если стиль описан так:
"cursor": {
"showTime": 0.2,
"hideTime": 0.2,
"color": "@colors.cursor.color",
"period12": 0.4,
"timingType12": "linear",
}
Стайлер преобразует его в такой класс:
@interface AGRCursorStyle : ASStyleObject
<NSCoding, data-preserve-html-node="true" ASStyleObjectApplyable>
@property (…) CGFloat showTime;
@property (…) CGFloat hideTime;
@property (…) UIColor *color;
@property (…) CGFloat period12;
@property (…) AGRConfigAnimationType timingType12;
@end
ASStyleObject
— это базовый класс всех стилевых объектов.
Работа со стайлером
Сначала нужно попросить стайлер прочитать стили из JSON'а:
ASStyler *styler = [ASStyler sharedInstance];
[styler addStylesFromURL:@"styles.json"
toClass:[AGRStyle class]
pathForSimulatorGeneratedCache:@"SOME_PATH"];
Префикс для классов, относящихся к стайлеру — "AS". У классов стилей в примерах префикс "AGR", потому что они взяты из проекта "Ангстрем".
В примере я сообщаю стайлеру, что класс AGRStyle
должен соответствовать корню стилей, все стили будут прописаны, как поля в нем. Последний параметр указывает, куда сохранять бинарный кэш стилей. Он обновляется в процессе каждого запуска приложения в симуляторе, позволяя быть уверенным, что на устройство попадет кэш с самыми новыми стилями.
Конечно же, лучше сделать обновление и кэша и структуры стилевых объектов на лету, в процессе редактирования файла стиля. Но это требует соответствующих плагинов для IDE, а их пока нет.
После инициализации можно получать стили вот так:
ASStyler *styler = [ASStyler sharedInstance];
AGSStyle *mainStyleObject = ((AGSStyle *) styler.styleObject);
AGRCursorStyle *style = mainStyleObject.cursor;
Этот вариант предназначен, чтобы получить значение стиля, использовать его и забыть.
Также можно вытащить значение вот так:
AGRCursorStyle *style = [[AGRCursorStyle alloc]
initWithStyleReloadedCallback:
^{
[self styleUpdated];
[self setNeedsDisplay];
}];
В этом случае мы не только получаем стиль курсора, но и подписываемся на его изменения. В случае, если стиль обновился, вызовется блок, где уже можно обновить UI, например, с помощью [self setNeedsDisplay]
, или другого, собственного, метода.
AGRCursorStyle
можно написать вручную, но лучше создать все классы стилевых объектов автоматически. Стайлер может делать это каждый раз при запуске симулятора (сохраняя в файлы ProjectStyles.h/m
) при помощи следующего кода:
[styler generateStyleClassesForClassPrefix:@"AGR"
savePath:@"[PATH_TO_CODE]/Styles/"
needEnumImport:YES];
При этом создадутся классы AGR[ГорбатоеИмяСтиля]Style
для каждого правила. Например, по правилу "editor" создастся класс AGREditorStyle
, а если внутри него есть правило "toolbar", будет создан класс AGREditorToolbarStyle
. Главный класс будет называться AGRStyle
.
Формат стилей
Файлы стилей — это обычный JSON. Полное название стилевого правила, если необходимо, составляется из его имени и имени родителей, через точку. То есть, если есть стили:
"editor": {
"cursor": {
...
}
}
То название стиля курсора будет "editor.cursor".
Стиль может ссылаться на другой при помощи следующего синтаксиса: "@another.name". Ссылка заменяется на конечное значение по ней.
"someStyle1": "value",
"someStyle2": "@someStyle1"
Также есть поддержка включений подфайлов стилей.
"@include.fonts": {
"inApp": "fontStyles.json",
"remote": "http://[SERVER]/fontStyles.json"
},
"Remote" — необязательная часть, в случае, если она есть, сначала грузится локальный стиль, и потом, в фоне, подгружается серверный, заменяя своими стилевыми правилами локальные.
Для указания типа стилей, используются префиксы:
- color для UIColor, Цвета можно указывать шестнадцатеричным значением, почти как в CSS (тремя одинарными шестнадцатеричными цифрами, шестью без альфы или восемью с альфой)
- point, origin, location, position, center для CGPoint,, указывается массив из двух чисел, x и y
- size или dimensions для CGSize,, указывается массив, width и height
- rect, frame, bounds для CGRect,, указывается массив из четырех чисел: x, y, width, weight
- margin(s), padding(s), border для UIEdgeInsets,, указывается массив из четырех значений: top, left, bottom, right
- font для UIFont,
- textAttributes для атрибутов NSAttributedString.
Например:
"margins": [23, 15, 10, 15]
"separatorColor": "@colors.about.separatorColor"
"appBackgroundColor": "#0f0d0a"
"labelRect": [0, 0, 120, 40]
и так далее.
Шрифты и атрибуты для NSAttributedString
— это объекты со строго определенными полями. Точки, размеры, прямоугольники, отступы — обычные массивы. Вот, например, описание шрифта:
"font": {
"name": "HelveticaNeue",
"size": 13
}
А вот описание атрибутов для NSAttributedString
:
"normalTextAttributes": {
"font": "@primaryFont",
"lineBreakMode": "NSLineBreakByTruncatingTail",
"color": "#990202"
}
(можно указывать и другие атрибуты, в примере показаны не все).
Также в стилях можно использовать "функции". Сейчас поддерживаются две:
~color.alpha(Цвет, Прозрачность)
~color.mix(Цвет1, Цвет2, Доля)
Первая изменяет прозрачность цвета, вторая смешивает цвет по формуле:
Результат = Цвет 1*Доля + Цвет 2*(1 - Доля)
Заключение
Я использую стайлер уже несколько месяцев и почти во всех новых проектах. Это помогает как лучше структурировать приложения так и быстро изменять дизайн в случае необходимости, что экономит время. Стоит упомянуть еще возможность работы со скинами приложения, но это, скорее, побочный эффект.
Если у вас есть какие-то пожелания, замечания или вопросы по стайлеру — пишите: alex@lonelybytes.com.