Layout: a Declarative UI Framework for iOS

Developing UI at scale can be challenging.

At Schibsted, our large, distributed team of iOS developers collaborates on a single codebase. Our modular UI is shared by several apps, each of which applies a unique theme. Our requirements are effectively the worst case scenario for using Storyboards and Interface Builder.

Our solution to these requirements is Layout, a new collaboration-friendly UI system for iOS.

The Challenge

We needed a system that:

  • Makes it easy for multiple developers to collaborate on the same screens and components, and to review and merge changes using standard tools.
  • Allows for faster UI development than recompiling the whole app after every change.
  • Supports runtime theming and customization.
  • Is modular and composable.
  • Is simple and familiar for existing iOS developers.
  • Can deliver over-the-air updates for bug fixes or A/B testing.

So what’s wrong with the tools that Apple provides with the iOS SDK?

The Trouble with Interface Builder

When Apple first released the iOS SDK in 2008, they included Interface Builder, a WYSIWYG drag-and-drop tool originally developed for laying out desktop application windows, now adapted for the iPhone touchscreen.

Right from the outset, the iOS developer community was divided on whether this was the right approach, and many eschewed Interface Builder in favor of creating views programmatically.

Interface Builder has come a long way since it was first introduced, but the reasons for not using it remain largely unchanged:

  • Nibs and Storyboards (the serialized UI files exported by Interface Builder) only support a subset of UIKit features. Any non-trivial screen will require parts of the layout or control wiring to be done in code, resulting in an awkward division of logic between the Nib/Storyboard file and source code, making it harder for developers to find where a change needs to be made.
  • The XML format for Nib files (xib) is chock-full of unique machine-generated identifiers, making it impossible to effectively edit, review or merge changes. They can only safely be worked on by one developer at a time, even when using Git.
  • Nibs cannot easily be subclassed or composed, and cannot reference code constants for fonts, colors, etc. All properties set inside Nibs and Storyboards are effectively “magic” values that cannot be assigned meaningful names, and can only be reused by copying and pasting them between screens that share common elements.
  • Changing the appearance of views at runtime (e.g. for night mode, or to support multiple themes) means creating code bindings or subclasses for each and every on-screen view, and setting the properties programmatically. Different themes can’t be previewed at build time, eliminating most of the benefit of Interface-Builder’s WYSIWYG preview.
  • The mechanism behind Interface Builder encourages use of implicitly-unwrapped optionals and string-based name/type binding that will crash at runtime if mistyped. This was onerous when using Objective-C, but is especially unpleasant when programming in Swift, which favors static typing and compile-time error checking.
Apple's Interface Builder

Apple’s Interface Builder

While these problems have remained unsolved since its inception, Interface Builder has acquired new ones:

  • When Nibs gave way to Storyboards, Apple encouraged developers to begin using IB for creating not just individual screens, but segues between screens as well, grouping multiple screens in a single Storyboard. This exacerbates the problem of file conflicts, as now developers cannot safely make edits to any screen in the app (or whatever subset of the app is contained within one Storyboard) without coordinating to avoid conflicts.
  • Interface Builder was designed before the introduction of AutoLayout, when the layout system used a more traditional “springs and struts” model called Autoresizing. IB encourages you to place views with drag and drop, but AutoLayout constraints completely replace that model, making it redundant. The relationship between the views shown on screen and the constraints that position them there is confusing and brittle, and making significant changes to layout usually requires removing all constraints and starting again.
  • IBDesignable, the new system introduced to solve the problem with previewing custom views, is flaky and doesn’t work well with code that is split across multiple modules, so a screen containing custom components often appears as a patchwork of empty squares in Interface Builder, rather than an accurate preview. It also puts the burden on developers to add extra boilerplate code to support design mode.

In the face of these problems, why not simply ignore Interface Builder and create views in code?

The Trouble with Hand-Coding UI

The most common alternative to using Interface Builder for UI is to create all your views programmatically. But this approach has its own problems:

  • The more code you write, the more bugs (or potential for bugs) you introduce into your program. The files generated by Interface Builder replace human-generated code with machine-generated data, and as such reduce the space for bugs to hide.
  • AutoLayout, Apple’s technology for specifying the size and positions of views on screen, is extremely long-winded to use in code. Despite the problems mentioned earlier, Interface Builder’s GUI approach makes it somewhat less cumbersome.
  • Being able to drag and drop components and see the result immediately in a (mostly) WYSIWYG interface is a huge productivity gain over having to stop, edit, compile and run every time you want to see the result of a change.
  • Apple expects developers to build their apps using Storyboards. Example code uses it. Default project templates are automatically set up to use it. Some features, such as localized app startup screens, are essentially required to use it.

This last point is perhaps the most important: Ultimately, any deviation from Apple’s status quo carries a cost.

The Trouble with 3rd Party Solutions

There are many existing 3rd party frameworks that address some of the issues, but none that meet all of our requirements:

  • AutoLayout wrappers such as Masonry help to speed up development when creating views in code, but they don’t avoid the fundamental problem of having to recompile after every change – a problem exacerbated by Swift’s relatively slow compile times.
  • Alternative development toolchains like React Native offer live reloading without recompiling, but at the cost of porting your app to JavaScript and React. Our iOS developers are happy with the power and flexibility of Apple’s UIKit and Foundation frameworks, and we already have a large codebase written in Swift, so switching to JavaScript would be a hard sell. It would also be a hard decision to reverse if it didn’t work out.

Over the years we have seen many such 3rd party development tools come and go, but the majority fail to gain significant traction because the pros so rarely outweigh the cons.

By adopting a nonstandard tool you run the risk that it will be slow to support new iOS features, or that its developers will stop maintaining it altogether. Even if it has a strong development community behind it, that community is a tiny fraction of the of the total iOS developer base – that impacts your ability to find developers who can work on your product, or to find solutions to bugs on Stack Overflow, and so on.

In light of that, it seems like creating our own UI framework may seem like an odd decision, so why build our own tool? In short, because the problems are real, and the potential benefits of solving them are worth the risk.

But if we are going to pay the cost of adopting a nonstandard tool, it better be something we are comfortable using, that meets all of our requirements, which we can maintain ourselves, and which is easy to migrate away from again if our requirements change.

That’s where Layout comes in.

Layout

Layout (available here from Schibsted’s github page) is a drop-in replacement for Nibs and (to some extent) Storyboards, based on human-readable XML files that can easily be edited and merged.

In place of hard-coded constraints, Layout offers dynamic, parametric layouts via the use of runtime-evaluated expressions (more on these later).

In place of WYSIWYG editing, Layout offers live reloading, so you can make tweaks and bug fixes to your real UI, in-situ, without recompiling the app.

In place of procedural, stateful view logic, Layout offers declarative data binding.

Like Nibs and Storyboards, Layout uses reflection to automatically recognize and bind components at runtime, so there is no need to create plug-ins for your existing view components – they will mostly just work without changes.

Unlike Nibs and Storyboards, Layout’s runtime bindings are fully type-checked and crash-safe if used correctly. Type or naming errors are detected statically and reported at the first opportunity, not deferred until a button is pressed or an outlet is accessed.

Layout is written in 100% pure Swift code. There is no reliance on JavaScript or any other resource-hungry scripting language.

Most importantly, Layout is not a replacement for UIKit. It is not a cross-platform abstraction, and it does not provide its own UI components – everything that Layout puts on the screen is either a standard UIKit view, or an ordinary UIView subclass that you have built yourself.

It is easy to add Layout to an existing project, easy to use existing components, and easy to remove it again if it’s not the right fit. You can use a handful of Layout-driven screens or components in an otherwise standard app, and it need not affect your app architecture in the slightest.

Expressions

Layout’s XML files describe view properties in terms of runtime expressions, powered by the Expression library. Expressions are simple, pure functions that allow dynamic values to be specified in a declarative fashion.

In recent years, the development community has come to the realization that traditional, procedural code that relies on mutable state is a difficult way to manage complex logic such as user interfaces. Frameworks such as Facebook’s React have demonstrated a simpler, more maintainable approach to UI based on the composition of pure functions that take state as an input and produce a view hierarchy as the output.

Apple’s own AutoLayout framework is also based on this idea, replacing complex procedural code with dynamic constraints that can describe flexible layouts mathematically instead of programmatically.  

So why doesn’t Layout just use AutoLayout for view positioning? It would be relatively simple to specify AutoLayout constraints in XML and rely on that for positioning views, so why reinvent the wheel?

Despite its name, the scope of Layout goes beyond merely laying out views – it can be used to configure essentially any property of a view, not just its size and position. And because these properties are specified in terms of expressions rather than static values, the ability to describe flexible layouts comes essentially for free.

So Layout doesn’t use AutoLayout because it would be redundant – you can replicate all the features of AutoLayout using expressions, as well as more complex behaviors that cannot easily be described in terms of AutoLayout constraints.

With that said, Layout does interoperate nicely with AutoLayout-based views. If a view has internal AutoLayout constraints, Layout will use those to determine its size.

Here is an example. This is the native Swift code in the view controller which loads the XML template and passes in some constants to use:

The XML describes a UIView containing a UIImageView and a UILabel:

All the dimensions here are flexible: The image and label are sized to fit their content; the container view is set to 100% of the width of its container, and its height is set to auto + 20, which means it will fit the height of the image view or label (whichever is larger) plus a 10-point margin on either side.

The image view’s width is determined to be whichever is the larger of its intrinsic width or height (determined by the source image), and the height is set to match the width, so that it is always square. The label is positioned 10 points to the right of the image view, and the image view and label are both vertically centered in their container.

The expressions used to calculate the value for each layout property should look familiar to any iOS developer – they are very much like the layout code you might have written inside a layoutSubviews method in the days before AutoLayout. But these expressions are not compiled Swift code, and they can be tweaked and debugged without rebuilding the app.

Below, you can see the resultant view as it would appear in portrait and landscape on an iPhone:

 

Live Reloading

Developing a pixel-perfect layout involves a lot of iteration, and developer productivity depends on making that process as fast and painless as possible. Layout’s solution to this is live reloading, a mechanism which allows Layout to reload XML files on-the-fly without recompiling the app.

Live reloading works by scanning your project directory for layout XML files and matching them up to the versions bundled in the built application. Layout then loads these files preferentially over the bundled file. If Layout cannot locate the file it will display an error, and if it finds multiple candidate files, you will be asked to choose which one it should use.

Layouts can be reloaded at any time using the Cmd-R keyboard shortcut when the simulator has focus.

Unlike React Native, Layout’s live reloading system is serverless, and does not rely on any background processes. The live reloading code is encapsulated inside your app, and only included in apps built for the simulator, as a real iOS device doesn’t have access to your Mac’s file system.

Layout in action

Layout in action

The Red Box

Almost any mistake you make in a Storyboard will result in a crash inside UIKit, leaving you hunting through the console log trying to work out where you went wrong. Layout’s live reloading feature wouldn’t be much use if every typo resulted in a crash, so instead we have the Red Box.

The Red Box (a concept borrowed from React Native) is a fullscreen error dialog that appears whenever you make a mistake in your XML. Whether it’s a syntax error, a misnamed property, or a circular reference in one of your expressions, the Red Box will tell you exactly where you went wrong, and a tap will dismiss it again once the problem has been corrected.

Layout’s error handling is enabled in production apps too, so you can intercept and log production errors, and then either crash or fail gracefully at your own discretion.

Over-The-Air Updates

While Nibs or Storyboards can be loaded from a remote URL, there is no official support for this in UIKit, and doing so is very unsafe, as a mismatch between app and Storyboard versions may result in an untrappable crash.

Layout supports asynchronous loading of remote XML files and has the basic infrastructure in place for over-the-air UI updates. Errors can be trapped and handled gracefully, so that the app can roll back to a safe state in the event of a bad update.

Convenience

There are a number of rough edges in iOS development, and Layout takes the opportunity to simplify those wherever possible. For example:

  • Fonts: Programmatically specifying a particular font at a given weight and style can be tricky; you need to know the exact font name and/or mess around with nested dictionaries of stringly-typed constants to specify the required attributes. Layout lets you specify fonts as a single space-delimited, CSS-style string that it parses to find the closest match. Font sizes can be specified in fixed or relative units, and Layout provides seamless support for iOS’s dynamic text resizing feature, even for custom fonts, which normally require you to calculate point sizes manually.
  • Colors: Colors are almost universally specified using a 6-digit hex format or a triplet of RGB values in the range 0-255, but iOS requires you to specify RGB values in the range 0-1. This makes sense mathematically, but it’s a pain to do this conversion manually. Layout lets you use 3-, 4-, 6- or 8-digit hex color literals directly in your XML, along with CSS-style rgb() and rgba() functions.
  • Rich Text: Creating attributed strings in code is hard work, and to date iOS still doesn’t provide any way to put styled text in a localized strings file. Layout lets you drop basic HTML straight into your XML file for doing simple styling like bold or italics, and if you use HTML in your strings file, those strings will be automatically converted to attributed strings when used with Layout components.
  • Table cells: Storyboards provide a convenient way to create cell and header templates directly inside your table. But if you later decide that you need to share those cells between more than one screen, that’s an awkward refactor. Layout lets you specify XML cell templates either inline within the table or in their own file, using exactly the same syntax, making it painless to refactor later.
  • Localization: Using localized strings inside Interface Builder is awkward as you have limited control over key names, there is no automatic way to add new strings after the initial extraction, and no warning if you forget or mistype a key. Layout lets you reference strings from your Localizable.strings file directly in your XML, and will display an error if any string is missing. It also applies the same live loading feature to strings as it does for XML, so you can add, modify, or remove strings and see the results without recompiling the app.

Roadmap

Though it has only been in development for a few months, Layout has already been integrated into several Schibsted apps, and is running in production on the App Store.

In the next few months, we’ll be focussing on:

  • Supporting more special-case iOS components, properties and types
  • Even more robust error handling, and improved unit and integration test coverage
  • Better tooling, such as static analysis, linting and refactoring for template files
  • Improving the examples and documentation

We’re really excited about Layout’s potential, which is why we’ve decided to open it up to the wider iOS community. Your feedback will help it to grow faster, and to expand it support new use cases beyond our requirements at Schibsted.

Links

The Layout framework on Github:

https://github.com/schibsted/layout

Expression – the library that powers Layout’s runtime expressions:

https://github.com/nicklockwood/Expression

Read more from the Software engineering category
SUBSCRIBE TO OUR UPDATES
Menu