Turning UI Specifications into Swift code
One of the challenges of building applications for Apple platforms is developing a clean way to capture the details of your UI design; fonts, spacing, sizes, colours and so on. You need these to be available to your app code to avoid hardcoding metrics and other attributes, and there are various ways to do this – whether you use Interface Builder or build your UIs in code.
We always strive to use the right language and concepts to talk about problems we face building apps, and design is no exception. Usually this means stripping away everything that is not directly related to the job at hand, to achieve something simple that anybody can understand without much effort.
Over the last few years we’ve iterated on a nice pattern for expressing UI Specs with Swift. We want to capture every detail of our design team’s gorgeous specifications in a way that anybody can understand and verify, while encouraging developers to do things the right way.
To be clear — we’re not trying to sell you on using a framework we've made. This is just a pattern for achieving this in Swift with some example code that you are free to copy and adapt to your needs.
Our goals were:
Consistency – we should define all our basic style constants exactly once, be they colors, fonts or dimensions.
Everything in one place — there should be exactly one place where all the constants related to a given UI feature are defined and anybody with access to the code base should be able to find and change them easily.
Grid-unit based dimensions — specifying margins, padding and sizes in multiples of this unit, e.g. 1/2 or 1/8th of a unit is a great way to design.
No explicit colors in code – we need logical colors that map to one of a small set of “swatches” that we use throughout the apps, so we can easily change the underlying colors without affecting lots of source files when
CoolRedColor needs to change to
Use static compilation – there’s no good reason to evaluate these things at run time, and IDE code completion helps a lot when writing the code to assign style constants.
To achieve this we use the idea of a “UI Spec” type that encapsulates all of the metrics and other attributes of a functional part of the UI, for example the “Onboarding screens” of an app. With some sneaky Swift shenanigans and use of types and access control, we arrived at UI specifications that are as straightforward as this:
As you can see there’s very little “noise” in the code, and we have namespaced constants thanks to the use of nested
enum types. There are several things to unpack about how this works in the above example, but for now let’s gloss over that and say that inheriting from
AppUISpec gives the onboarding UI spec access to the stylistic building blocks that apply across your app;
For bonus hipster points, if you are so inclined, you could even add a few sneaky
typealias(es) and use Emoji to select fonts, colours and dimensions:
…and then re-writing the onboarding spec to use these, it becomes very visual and less verbose. OK, and yes it becomes harder to type but a bit of copy and paste goes a long way.
Yes, it’s a matter of taste and no, we don’t use this style ourselves currently. Either way, your UI code can use these values like this:
As you can see the values from the UISpec have handy helper functions to make applying these values to user interface elements very simple. Notice how you can easily see in the code which element of the UISpec is being applied to the various UI elements. If you needed to find where in the app the
OnboardingUISpec styles are used, it is a simple case of searching for uses of that type.
If you don't like the verbosity of the namespacing you add a local
typealias for any level of the UISpec's nested definitions, such as
typealias Spec = OnboardingUISpec.Card. If your nested types conform to a protocol of your own for say Light vs. Dark mode, you can even pass the UISpec's nested type itself around to code – all while keeping compile time type safety!
In terms of how this works under the hood, it helps to look at the “root” UI spec in this case:
Use of Swift
typealias and access control in the base UI Spec as well as in the metric types
GridDimension allow us to lock down what developers can do by accident. You can place the UI Spec code into a common framework you use across your apps, and the internal access means developers can't "work around" the system if that is important to you.
For example there are no public initialisers on
GridDimension so you’d have to go out of your way to create your own metrics on-the-fly in App code. However that won't work for you if you want your base UI Specs in a separate framework and your App-specific UI Specs in the Apps themselves - you'll have to make those types
public in that case
We won’t go into the details of the metric types used here – there’s nothing particularly complicated going on, but the addition of static helper functions allows the UISpec to be clear and concise. In addition we try not to go too far down the Swift generics rabbit-hole. I'd like to call out two particular Swift tricks that have helped:
- Local typealiases. You can nest these in other types, but also inline in code. This helps cut down code noise while keeping the benefits of static typing.
ExpressibleByFloatLiteraland related protocols in the standard library. Conform to this and your type can just be assigned a float. See
Through judicious use of some basic Swift generics, access control, protocol extensions on the single-property protocol
UISpec we end up with an elegant set of tools to define, discuss and refine our UI implementations with our design team.
There's a lot more we can do with this pattern in the future, with scope for a
TextStyle type that combines font, colour and text alignment, and attributes for attributed strings. We'll keep iterating!
The full code of a working example app is available here on Github.