Что такое 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 код будет «в тему».

Комментарии (10)


  1. vintage
    10.03.2016 13:10

    Думаю тут стоило идти по пути совместимости с CSS, а не JSON.


    1. bealex
      10.03.2016 13:13

      Про CSS я думал. Получилось, что они очень плохо ложатся на UI мобильных приложений. Впрочем, попытки применения CSS к ним уже были (например, https://nativecss.com).


      1. vintage
        10.03.2016 14:38

        А в чём сложности?


        1. bealex
          10.03.2016 15:09

          Никаких сложностей, просто, по моему мнению, CSS не подходит для нативных UI мобильных устройств, чем я занимаюсь.


          1. vintage
            10.03.2016 18:33

            Почему это?


            1. bealex
              10.03.2016 19:13

              Короткий ответ — потому что он сделан для другого. Каскадность совершенно ни к чему, нет, как таковой, иерархии блоков, нет DOM'а, чтобы стили привязывать к элементам, лейаут делается совершенно по иным правилам. Это только те причины, которые на поверхности, реализовывать CSS для нативного использования тоже не очень приятно.


              1. vintage
                10.03.2016 19:38
                +1

                Не путайте CSS как язык, и CSS как синтаксис. В чём проблема взять синтаксис и часть языковых средств (определения цветов, отступов), выкинув ненужное (каскад, стандартные раскладки) и добавив недостающее (например, вложенные определения из less/scss/stylus)?


                1. bealex
                  10.03.2016 20:07

                  Извините, я никак понять не могу. Вы в принципе не признаёте иных решений, кроме тех, которые знаете, или просто желаете набросить?

                  Если вам требуется CSS, используйте конечно. Я ведь не заставляю использовать свою систему. Для этого и существуют разные варианты, чтобы иметь возможность выбрать нужный в конкретном случае. Впрочем, можно всегда пользоваться только микроскопом, это тоже валидное решение.


                  1. vintage
                    10.03.2016 22:05

                    Вы зря воспринимаете критику в штыки.


                    1. bealex
                      10.03.2016 22:18

                      В ваших комментариях нет критики :-)