Система стилей Ангстрема

Создание мобильного приложение — это итеративный процесс. Дизайн начинается в Фотошопе, а разработка — в АппКоде. Но когда первый прототип готов, дизайн и программирование объединяются: для практически любого изменения в интерфейсе необходимо изменить код. Для изменения цвета, шрифта или настройки параметров анимаций, дизайнер просит разработчика о помощи. Это взаимодействие замедляет процесс, а некоторые эксперименты из-за него получаются слишком затратными.

Когда мы с Ильёй начинали делать Ангстрем, то сразу решили создать гибкую стилевую систему, чтобы упростить подстройку дизайна в процессе разработки.

Базовые принципы

Стилевые файлы хранятся отдельно от кода, они должны легко читаться, редактироваться в любом редакторе и парситься приложением. Поэтому в качестве формата я выбрал 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.