Ångström Style System

Building an app requires iteration. Photoshop is a good place to start designing, AppCode is a good place to start developing, but when the first prototype is ready, design and development merge: most changes require coding. When the designer wants to adjust colors and fonts, refine interactions, he asks the developer to do it. This slows down the process and makes experimenting prohibitively costly.

When Ilya and I started making Ångström, we decided to create a flexible style engine, so that tinkering with the design would be easy for both of us.

Design considerations

The stylesheets must be stored separately from the code, be easy to read, write and parse. That’s why we use JSON, not Plist or CSS and keep them in a Dropbox.

The styles must reload on demand in the running app. No recompiling, no reinstalling, no restarting. That’s why we have Shake to Refresh.

The app must open fast no matter how long the stylesheet is. Ångström has ended up having hundreds of style rules, bigger apps will require thousands. That’s why we compile styles into binary.

The code related to styles should be easy to maintain. That’s why we do not address the style variables by string literals, i.e. [styler boolForKey:@"isTopBarHidden"]. A typo would end up being a debugging nightmare. Instead, we create an object with the corresponding fields, style.isTopBarHidden, and let the compiler do its job looking for errors.

Architecture

Ångström Style System has three objects:

  • Styler loads and stores stylesheets, sends notifications if style changes.
  • Style Objects are created by the Styler from the stylesheets and contain typed style values.
  • Style Listeners receive and process notifications on style changes.

Styler is able to generate Style Object classes automatically from JSON. After that it can:

  • Use NSArchiver to save and restore styles in the binary form (for startup speed)
  • Get the values from this class the fastest way possible, simply by accessing its properties (for general application speed and convenience).

For example, style system will transform this JSON:

"cursor": {
    "showTime": 0.2,
    "hideTime": 0.2,

    "color": "@colors.cursor.color",

    "period12": 0.4,
    "timingType12": "linear",
}

into this class:

@interface AGRCursorStyle : ASStyleObject
    @property (…) CGFloat showTime;
    @property (…) CGFloat hideTime;
    @property (…) UIColor *color;
    @property (…) CGFloat period12;
    @property (…) AGRConfigAnimationType timingType12;
@end

ASStyleObject is the base class for all Style Objects.

Workflow

Before using the Styler, we need to parse JSON with the styles definitions:

ASStyler *styler = [ASStyler sharedInstance];
[styler addStylesFromURL:@"styles.json" 
   toClass:[AGRStyle class] 
   pathForSimulatorGeneratedCache:@"SOME_PATH"];

ÅSS classes have prefix "AS". Example classes are prefixed with "AGR" for "Ångström".

Here I use the AGRStyle class as the root class for all my styles. This is the main Style Object. The last parameter is for automatic saving of the binary cache (serialized AGRStyle instance that is used to speed up the application startup). It is saved during Simulator launch and allows me to be sure that this cache is up to date.

Of course this cache must be updated right after style was changed. But this requires IDE plugin.

You can get the style value like this:

ASStyler *styler = [ASStyler sharedInstance];
AGRCursorStyle *style = 
    ((AGSStyle *) styler.styleObject).cursor

This variant allows to get the value and forget about it. A good choice If it is needed just once.

Also you can get the same style another way:

AGRCursorStyle *style = [[AGRCursorStyle alloc] 
   initWithStyleReloadedCallback:
        ^{
             [self styleUpdated];
         }];

This choice allows to create a callback, that will be called when the style is updated. It's a good place to send some a message like [self setNeedsDisplay]; or something that will restyle your view.

After that you can use the style object as a usual Objective-C object.

How can you create this class? When the Style System runs in the Simulator, it can generate all the classes from JSON structure for you and write a ProjectStyles.h/m files with them. Autogeneration can be done with this line of code:

[styler generateStyleClassesForClassPrefix:@"AGR"
        savePath:@"[PATH_TO_CODE]/Styles/"
        needEnumImport:YES];

It will generate AGR[CapitalizedStyleName]Style classes for every style rule in the JSON file. For example, a style rule "editor" will generate a class AGREditorStyle, and if the "editor" style rule contains "toolbar" subrule, it will become AGREditorToolbarStyle. Main class will be AGRStyle.

Ideally, the class generation has to happen in the IDE on the fly during JSON editing, but it requires an IDE plugin and some more coding time. I hope to do this in the future.

Stylesheet format

Stylesheets are hierarchical. Any style can be referenced via all it's parent styles like this: superstyle.parentstyle.style.

Styler supports references. If a value looks like "@another.name", then it is replaced with the value of "another.name" style.

Also there is include support. That allows creating large and detailed file with all the styles and smaller, simplier files for remote editing. Here is the example:

"@include.fonts": {
    "inApp": "fontStyles.json",
    "remote": "http://[SERVER]/fontStyles.json"
},

Includes have optional remote part that will load file from the remote server. Local part is for production, remote for development.

Style name suffixes define value types:

  • color for UIColor,
  • image for UIImage,
  • point, origin, location, position, center for CGPoint,
  • size or dimensions for CGSize,
  • rect, frame, bounds for CGRect,
  • margin(s), padding(s), border for UIEdgeInsets,
  • font for UIFont,
  • textattributes for NSAttributedString attributes (in the end it is a simple NSDictionary but with specific keys).

For example:

"margins": [23, 15, 10, 15]
"separatorColor": "@colors.about.separatorColor"
"appBackgroundColor": "#0f0d0a"
"listTapColor": "@colors.activeColor",

and so on.

Fonts and TextAttributes are dictionaries with specifically named parameters, points/sizes/rects/margins are simple arrays. Here is a font example:

"font": {
    "name": "HelveticaNeue",
    "size": 13
}

And this is a definition of a dictionary for NSAttributedString:

"normalTextAttributes": {
    "font": {
        "name": "HelveticaNeue",
        "size": 18
    },
    "lineBreakMode": "NSLineBreakByTruncatingTail",
    "color": "#990202"
}

You can define other NSAttributedString attributes too.

Style also supports "functions". Right now there are only two of them:

~color.alpha(COLOR, ALPHA)
~color.mix(COLOR1, COLOR2, PART)

First one adds (or replaces) alpha channel to the color, second one takes PART from COLOR1, (1 - PART) from COLOR2 and adds them to get a new color. Something like this:

RESULT = COLOR1*PART + COLOR2*(1 - PART)

Conclusion

I am already using ÅSS in other projects and it helps a lot. If you have any questions or suggestions, drop me a line: alex@lonelybytes.com.

Can’t innovate anymore, my ÅSS! Sorry, could not help :-)

Mastodon