Last week people realized that Xcode 8.3 by default uses storyboards in new projects without a checkbox to turn this off. This of course sparked the Interface Builder vs. programmatic UI discussion again, so I wanted to give some insight in our experience using Interface Builder in building the Lyft app. This is not intended as hard "you should also use Interface Builder" advice, but rather to show that IB can work at a larger scale.
First, some stats about the Lyft app:
- 40 storyboards
- 100 XIB files
- 150 view controllers
- 20 people on the iOS team with occasional outside contributors
With the rewrite of our app we moved to using IB for about 95% of our UI.
The #1 complaint about using Interface Builder for a project with more than 1 developer is that it's impossible to resolve merge conflicts. We never have this problem. Everybody on the team can attest that they have never run into major conflicts they couldn't reasonably resolve.
With that concern out of the way, what about some of the other common criticisms Interface Builder regularly gets?
Improving the workflow
Out of the box, IB has a number of shortcomings that could make working with it more painful than it needs to be. For example, referencing IB objects from code still can only be done with string identifiers. There is also no easy way to embed custom views (designed in IB) in other custom views.
Over time we have improved the workflow for our developers to mitigate some of these shortcomings, either by writing some tools or by writing a little bit of code that can be used project-wide.
To solve the issue of stringly-typed view controller identifiers, we wrote a script that, just before compiling the app, generates a struct with static properties that exposes all view controllers from the app in a strongly-typed manner. This means that now we can instantiate a view controller in code like this:
Not only is
viewController now guaranteed to be there at runtime (if something
is wrong in the setup of IB the code won't even compile), but it's also
recognized as a
SignUpViewController and not a generic
All our view controllers have a base view controller named
This base controller implements
prepare(for:sender:) like this:
1 2 3 4 5 6 7 8 9 10 11
This means that a view controller that has a segue to
TermsOfServiceViewController can now do this:
1 2 3 4
We no longer have to implement
prepareForSegue and then
switch on the
segue's identifier or destination controller, but we can implement a separate
method for every segue from this view controller instead which makes the code
much more readable.
We wrote a
NibView class to make it more convenient to embed custom views in
other views from IB. We marked this class with
@IBDesignable so that it knows
to render itself in IB. All we have to do is drag out a regular
the object library and change its class. If there is a XIB with the same name as
NibView will automatically instantiate it and render it in the
canvas at design time and on screen at runtime.
Every standalone view we design in IB (which effectively means every view in our
app) inherits from
NibView so we can have an "unlimited" number of nested
views show up and see the final result.
Since a lot of our views have corner radii and borders, we have created this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
This lets us easily set these properties on any view (including the ones from UIKit) from Interface Builder.
We wrote a linter to make sure views are not misplaced, have accessibility labels, trait variations are disabled (since we only officially support portrait mode on iPhone), etc.
A bug impacting developers
that use Interface Builder on both Retina and non-Retina screens (which at Lyft
is every developer) has caused us enough grief to write
ibunfuck - a tool to remove
unwanted changes from IB files.
We created a custom color palette with the commonly used colors in our app so it's easy to select these colors when building a new UI. The color names in the palette follow the same names designers use when they give us new designs, so it's easy to refer to and use without having to copy RGB or hex values.
In addition to these tools and project-level improvements, we have a number of "rules" around our use of IB to keep things sane:
- Do as little as possible in code, do as much as possible in IB. That means fonts, colors, images, and other properties should not be set in code if they don't change at runtime.
@IBInspectableon custom views whenever possible.
- Create as much Auto Layout constraints in IB as possible. Sometimes this means being clever with how we create constraints so that at runtime we need very little code to manipulate a layout.
- If we do need to create constraints in code, we don't use the
NSLayoutAnchor) methods directly. Instead we use SnapKit's convenience methods.
- We still manually push and present view controllers where possible. Even though we have created some niceties around using segues, it's still easy to accidentally remove one and not find out about it until runtime.
- We tend to create a new storyboard when we recognize a common theme in related view controllers. Just as with code, we don't want bloated storyboards with all kinds of unrelated things in there. Most of our storyboards have fewer than 10 scenes.
- Our outlets are defined carefully.
Of course, even with these improvements everything is not peaches and cream. There are definitely still problems. New versions of Xcode often change the XML representation which leads to a noisy diff. Some properties can simply not be set in IB meaning we're forced to break our "do everything in IB" rule. Interface Builder has bugs we can't always work around.
However, with our improved infrastructure and the points from above, we are happy with how IB works for us. We don't have to write tons of Auto Layout code (which would be incredibly painful due to the nature of our UIs), get a visual representation of how a view looks without having to run the app after every minor change, and maybe one day we can get our designers make changes to our UI without developers' help.