In 2016, we rebuilt the HubSpot app in Swift. Storyboards featured heavily for half the year. At first they appeared to yield benefits, but we began to get the sense we were invested in an anti-pattern.

This is a collection of retrospective discoveries from the new world without xib files.

We gave them a fair chance

@IBDesignable , segues, storyboard references and more. We made every effort to come up with terse, expressive, composable code that benefitted from storyboards.

We were coming up with creative approaches, attempting to improve code reuse and improve clarity. Our view controllers became affected partners in the couple, with implementation details hidden in xib files, from us and from the compiler. So we jumped ship.

A code sample of how we deal with view controllers and views at the moment, to give some context on where we are:

For our team, this is many times clearer than anything we could produce with storyboards. We included some of our style-extensions; along with the mutation operator; and layout-extensions which appear at the bottom of this post. This is intrinsic to the post. The alternative to storyboards isn’t great if you don’t invest and iterate in another direction.

So, what did we appreciate in retrospect?

Old News (Even if you use Objective C)

Compile time assurances/Refactoring

Compilers are useful, we should use them. The app crashing at runtime because of an unconnected outlet isn’t a very sensible situation to put ourselves in.

Refactoring is kind of a big deal. If it’s not done constantly, code begins to creak. Multiple schools of thought on improving refactor-safety exist, but it’s fair to say that storyboards make refactoring more error-prone, due to the the loss of compile time checks.

String identifiers, tags, outlets etc need to line up in the storyboard and the code at runtime, which given a big refactor could be almost impossible.

Freedom of Design/Component Driven Apps

IB creates a mentality of restricted optionality. It has strict syntactic contracts and strong opinions. Since dropping it, we’ve experienced a renaissance in code reuse, with progressively more expectable and simple interfaces. It’s important to have the flexibility in development to allow for these expressions and simplifications to reveal themselves.

One way to do things

You can’t do everything in storyboards. Some things are there, and somethings are in code. The codebase should be expectable and deterministic over time, but IB adds this constant back and forth of “how much should we use storyboards?”.

There will be disagreement of what should be done in storyboards. Situations occur where the same things are done different ways across code and a wysiwyg interface, depending on the familiarity and experience of the engineers.

What did Swift offer?

Immutable and Non-optional Properties

We can’t affect IB calls to initWithCoder . As a result, every parameterised property on the view or view controller must be variable. Worse again, they must default to some value before receiving the value from the object’s owner. A lot of times this is nil , making the type Optional .

It’s useful to know values are not Optional and not var , the code should allow for this. This was not an issue in Objective C when this was the way of all things. It’s a drawback now we have these features baked into Swift.

Generics

Here’s a abstraction of a UITableViewController that can display a table of cells with strongly typed models, with a little help from this extension:

This is pretty useful code, but it’s invisible in storyboards. Do we disallow it from our codebase or just not use storyboards here? Also, notice the immutable cell models and success handlers, that have non-optional types. Lovely stuff.

The benefits of storyboards

Why we think this anti-pattern is a design pattern.

A comforting visualisation

You see a version of the interface. Without all the information in the side menus, this is not the actual interface. If two views appear side by side on the storyboard, we might assume they have some constraint making that so. It’s only when clicking in and inspecting the constraint you can verify that this is actually true.

We make assumptions of correctness based on coincidences in the layout, this doesn’t alway work out in our favour. In code we actually consider the rules that govern the layout and develop our own abstractions around these rules rather than the way it appears in the storyboard UI.

IBDesignable

You’re investing time and resources in making your custom class look right in interface builder. That’s arguably a waste of that time and those resources. It’s also hard to debug when IBDesignable crashes, leaving you with blank storyboards.

Boilerplate gets added for no runtime-functional benefit, just to enable a near-correct version of the UI for inspection without having to compile. What we see will never be a representation of the UI at runtime, because it doesn’t follow the same code-paths. Maintaining this consistency is time consuming and error prone.

Designer friendly

There are designer focused products like Sketch and InVision that can produce UIs and style guides. It’s hard to understand why any designer would like to use or should be made use Xcode. The engineer can implement the style guide in styling-extensions that handle all the implementation.

This produces composable building blocks for the UI that have been tested and verified to adhere to designs without having to recreate these in each storyboard.

We invest in this one implementation and make it the primary button, which is great for any redesigns that might pop up over time as we can just change the colour in one place. Think of it as UIKit-CSS.

HubSpot went through a design refresh while we were invested in IB. This pain was one of the first red flags to find a better way.

Segues

Segues don’t simplify code. They obfuscate control flow and depend on runtime strings across two very different interfaces lining up. If you’ve ever tried refactoring a segue and had an unexpected runtime crash you know what I mean.

A few problems with the above code:

The segue version depends on the string value of the side menu in the storyboard being "doTheThing" (Compile time assurances/Refactoring)

(Compile time assurances/Refactoring) An optional typecast gets involved, because everything coming out of a segue starts as a non-descript UIViewController .

. Even though param should never change during the objects lifecycle, there is no such syntactic expressions available anymore. (Immutable and Non-optional Properties)

should never change during the objects lifecycle, there is no such syntactic expressions available anymore. (Immutable and Non-optional Properties) The first example is only the passing of param , as mentioned before, this parameter is likely an Optional var . If something goes wrong the segue will carry on without param .

, as mentioned before, this parameter is likely an . If something goes wrong the segue will carry on without . The second example is everything, there is no other context.

This article from 2013 articulates the issues very well.

In Summary

We’ve accepted a long time ago the limitations of interface builder to be standard acceptable tradeoffs. I don’t think they are acceptable in the context of Swift and we should be looking for alternatives.