A Grand Unification Theory of iOS Styling
If you've been on a team developing iOS apps before, you've probably been involved in a discussion or outright debate about how to handle styling, or the visual appearance of the app. Here, roughly, are the three sides of that debate:
1. We should style everything directly in storyboards / Interface Builder
It's easy to do, saves the time and error of setting the properties in code, and is easy to see and maintain, because the storyboards reflects the fonts and colors as they will appear in the app.
- Easy to use, easy to understand what styles are applied where, because we can see it visually
- Don't have to write a bunch of boilerplate code, meaning less noise, less possibility of bugs
- We can see roughly what something will look like at runtime without having to build the app
- Changing a style throughout the app down the road is a nightmare. If all buttons are green today, and have to become blue for the next release, the only way to accomplish this is to go through every storyboard and screen, select each button individually and change the color. And it's really easy to miss one or two along the way.
- Only some properties can be set in Interface Builder. Many properties cannot, like corner radius, borders, and more. Those properties still have to be set in code.
- For properties that have to be set in code, or using runtime attributes in Interface Builder, the way those views are rendered in the storyboard will not reflect those properties. So what you see in the storyboard is just partially true / somewhat representative of what you will see at runtime. And often a partial truth is even more confusing than a known lie.
2. We should code our own styling or theming system.
Even though there is an investment in time and cognitive overhead to create and get everyone to learn the system, it has strong benefits. First, it saves a lot of duplicate code, because setting the same group of properties for a button to the same values can now be done with a single line of code that sets the style, rather than 6 separate lines of code whenever a button is instantiated. Second, it allows us to change a style in one place, and have it take effect throughout the app (for example, turning green buttons to blue buttons).
- More code reuse
- More guaranteed consistency because the same style will always be the same on every view, whereas setting colors in Interface Builder leaves room for one-off errors.
- Centralized style updates
- More work upfront
- Hard for new developers and even the same developers down the road to understand what a style looks like or which style name represents what they are going for. Example: "I want this button to be blue like in the comps, what was the name of that style again? Is it this "NormalButton" style? Or is it "ActiveButton", "LargeButton" or even "GreenButton" since buttons used to be green?
- Storyboards will no longer represent in any way what types of fonts, colors, button styles and visual appearance will be present in the app, which is a disconnect and large missed opportunity
- Seeing styles take effect to tweak or adjust them requires compiling and running the app, and navigating to the screen you are working on, which adds up to a lot of time.
- It's still easy to forget to style a button or a label, because you can't tell from the storyboard which views are styled or not. It happens entirely in code at runtime.
- You have to create @IBOutlets for every view in every view controller so you have a reference to it in order to set the style in code at runtime.
3. We should use one of those nifty libraries like Pixate Freestyle or NativeCSS that allow us to style our app dynamically using CSS stylesheets.
Using third party libraries means we don't have to write the code ourselves, and they are generally more tested and debugged by more people than our own internal code will be. Having CSS stylesheets is awesome, because we can change styles and adjust the theming of the app even after it is released, by having it load the stylesheet dynamically from the web at runtime. This means less work and hassle and release cycles just to update UI since it can be done by updating a single file on a server somewhere.
- Can get styling while writing little-to-no code.
- Update in one place, style gets changed everywhere
- Can change styles remotely without having to update and resubmit the app. Also allows per-user themes, or customizable white label solutions.
- CSS can already be very confusing, particularly for non-web developers, and it's often challenging to target just the exact views you want to be effected by a certain style. Cascading style inheritance just adds to the bag of hurt making it even more difficult to reason about what properties will eventually be applied to a view. It's even more confusing because these libraries have their own unique limitations and rules about what aspects of CSS will and won't work or might work differently with the library.
- CSS doesn't map directly or even at all to many types of UIKit properties. For example, there are no CSS properties that represent things like different text colors for button titles in different states (normal, highlighted, disabled) or that map to the different content display modes of a UIImageView, the iOS concept of dynamic text, etc.
- Still no way to see the results of styling in storyboards, which will not resemble to the running app. Still require compiling, running and navigating to the screen you are working on to see what a style will look like. This means that it's still easy to overlook elements that you forgot to style.
- Difficult to change stylesheets at runtime or have multiple stylesheets, since these libraries tend to work only with a single CSS file at a single location, and don't really support changing styles or stylesheets from code after that single CSS file has been applied.
There are some less common positions as well, such as using UIAppearance, which is sadly too limited and awkward to be of much use and still has all the drawbacks of other style-in-code solutions. And of course some people are content to just set the same properties of the same types of views manually in the code of all their view controllers over and over. And that approach has only the downsides of all the above approaches, and no upsides other than not needing to use a library or write the code needed for a reusable solution.
The Game Changer
For as long as iOS development has existed, those three options were pretty much the only and mostly mutually-exclusive approaches possible. But two years ago, as part of Xcode 6, Apple introduced two new ways of connecting code with Interface Builder: IBDesignable and IBInspectable.
These two annotations allowed developers to render their own individual custom views live in the storyboard, similar to how UILabels, UIButtons, UIImageViews, etc. have always been able to do, and also to expose certain properties to be settable from the Interface Builder attributes inspector. And while Apple intended these as tools for configuring custom views, they are actually remarkable appropriate for use in styling systems that finally bring together the different sides of the styling debate into a single win-win-win approach.
Although IBDesignable and IBInspectable have some narrow limitations and rough edges, they finally enable styling systems which:
- Allow styles to be defined one place in code, and reused throughout the app
- Allow those same styles to be added to views using the IB inspector panel in a storyboard, and be rendered live on the storyboard without needing to compile and run the app
- Allow for essentially the full range of view styling to be rendered in Interface Builder, even view properties that were previously never reflected like cornerRadius, borderWidth / borderColor and tintColor.
And so, I'm pleased to throw a new, and hopefully debate-resolving library into the ring: Stylish.
This new library combines the advantages of all the previous approaches has almost none of the downsides, and provides a fully-featured and fully-customizable solution for iOS app styling. Here's a quick rundown of what Stylish can do:
- Define styles and stylesheets in native Swift code, or in an external JSON file
- Styles can update any property of any view, including your own custom views
- Written 100% in Swift, Stylish gives you strong typing and compiler verification when used from code. No such guarantees for JSON, although type mismatches will be ignored instead of causing a crash.
- Multiple styles can be combined together for any view
- Styles can be added to views right inside your storyboard, and the views will update in real-time to reflect what styles have been added or removed.
- No need to create IBOutlets for any view you want to style - just set the style in the storyboard
- Immediately understand which style does what, because the view updates in the storyboard as soon as the style is added. Now it's easy to quickly iterate on style definitions and see the results as you work
- Stylish will add a red error pattern to any views on the storyboard that are set with a missing or invalid style
- Stylish styles can be applied just as easily in code using the syntax
myView.styles = "StyleOne, StyleTwo", and setting a new style in code at runtime will immediately update the view.
- Views can either use the current global stylesheet, or a different stylesheet if specified
- The global stylesheet can be changed while still working on storyboards / at design time by changing the "stylesheet" value in the app's info.plist. The global stylesheet can be changed at runtime by updating the contents of the file stylesheet.json in the app's documents directory, or in code using the syntax
Stylish.GlobalStylesheet = instanceOfDifferentStylesheet. You can also change the contents of stylesheet.json at runtime or design time and the app or storyboard will update immediately to reflect the updates to the JSON. Changing the stylesheet can instantly change the entire look of your app with a single line of code!
- Finally, views in your storyboards will appear exactly the same as they will be rendered at runtime. See the comparison at the end of the animated gif screen capture above. This means that you won't overlook unstyled elements and you won't need to compile -> run -> navigate to every screen to see how the styling will look.
Stylish is free and open source. You can download or contribute to the library at its home on GitHub:
I'm really looking forward to hearing what you think of Stylish and this new approach to styling in iOS. Sound off in the comments with any questions or feedback!