Stitch Fix recently released its first iPhone app! What started as a simple, single-storyboard application is now a complex application with entirely programmatic views. The transformation from storyboard to programmatic views was not straightforward–we experienced the good and the bad of storyboards, .xibs, programmatic views, and in-between hybrids.
The Storyboard
When I joined Stitch Fix in April, the iPhone app’s main feature was its onboarding flow. Users could sign in or sign up. The sign-up flow included several screens, which were a mix of webviews and native views. Once a user signed in, or went through the sign-up flow authenticated, they could see a webview. It was identical to what she’d see on our website.
At this point in time, the whole app was contained in a single storyboard. A storyboard made sense–there were only 2 flows that a user could go through, and they could be easily modeled through simple storyboard segues.
Half the application was webviews. We had some native views, but none of their styling was reused–there was nowhere to reuse it. The storyboard was fine for our simple application!
Even then, there were pitfalls to using a storyboard. Storyboards take view controller initialization out of the developers’ hands. This makes dependency injection a challenge. Without dependency injection, it was hard to test view controllers in isolation. There were a few workarounds for this, but they left us with a vague sense of guilt. We knew that declaring properties as var
’s instead of let
’s, just to overwrite them during tests, was not very Swifty.
There were also logistical issues to using storyboards. Merge conflicts were inevitable. As humans, we were not very skilled at resolving conflicts in these complex, machine-generated XML files.
We knew that storyboard segues could not represent the flows that we planned to build. What if a user wanted to edit her shipping address during the sign-up process? Or search for a new one? What if she quit while signing up, and then came back to resume sign-up? Eventually, we planned to build an account management section of the app. Users would be able to edit their saved payment and shipping information–we’d want to reuse the same form as our sign-up flow used. However, our view controllers were too tightly coupled to the onboarding flow. They were not easily reusable.
Using a storyboard was not sustainable. Without reusable, easily testable view controllers, we could not be confident about how the view controllers would behave in various contexts. We decided to move to an approach that would isolate each view controller.
.xibs
We chose to isolate each view controller’s view in its own .xib. This allowed us to control its initialization, which made testing easier. Most of our initialization methods looked something like below:
init(someService: Service,
someOtherService: OtherService) {
self.service = someService
self.otherService = otherService
super.init(nibName: nil, bundle: nil)
}
Service
and OtherService
were both protocols. Our test suite included fake versions of these dependencies. Our tests checked that the view controller used these dependencies correctly. (For a detailed look at how we test view controllers, check out this talk I recently gave.)
By moving from storyboards to .xibs, we were able to move navigation logic out of Interface Builder (IB), and into an explicit “navigator” object. This was a pattern inspired by VIPER’s concept of a router. It was fantastic–navigation flows were decoupled from the views they contained.
Our process was golden. With .xibs, we were able to move quickly. It was easy to visualize what a view would look like at runtime, because it was right there in IB. We had fewer merge conflicts than before. We were able to separate view controller tests from navigation tests.
The views, each isolated to its own file, had small inconsistencies in fonts, colors, and spacing. This was okay, because our designs were not finalized yet.
One downside of using .xibs was that certain solutions had to be repeated across many files. Most of views scroll vertically. To ensure that they only scrolled vertically, and not horizontally, we used the strategy outlined in the first comment on this blog post. It required a deep view hierarchy, something like: View > View > Scroll View > Scrollable Content. It was ugly, and each time we created a new scrollable view, we spent time trying to replicate and debug this hierarchy.
As designs for the app were finalized, the .xibs became hard to work with. IB does not provide a way for specifying common layout or constraint patterns. There was no way to specify that when a big red button is on top of a small blue button, there should be 4pts of spacing between them.
We had some properties, like font kerning and attributed placeholder text, which cannot be expressed easily in IB. We tried declaring these properties as IBInspectable
and IBDesignable
, but doing so caused XCode to frequently crash. Consequently, we moved more view setup entirely into our Swift files.
We had Frakenviews. They were half code, half IB. Interface Builder no longer represented what the app would look like at runtime–it looked more like a wireframe. When making a change, it was never obvious whether to look in the .xib or in the code.
Programmatic UI
Frustrated by Interface Builder’s inability to express our design language, we started refactoring our views to be entirely programmatic. Text styling became trivial, view components became more reusable, and XCode crashed less.
The transition to programmatic views became even easier as we wrote helper methods to abstract the verbose autolayout methods. These methods were perfect for expressing our design patterns. For example, to describe the relation of several vertically stacked elements, we wrote something like below:
headerView.beginVerticalLayout(topMargin: 10)
.onTopOf(anotherView, spacing: 12, height: 24)
.onTopOf(someOtherView, spacing: 8, height: 24)
.finishVerticalLayout(bottomMargin: 10)
(Xcode 7 beta was already available when we wrote this code, so we tried hard to write something that could eventually be refactored into a UIStackView
.)
We even wrote helper methods that specifically described small, repeated aspects of our layout. For example, to describe 2 buttons stacked on top of each other:
//primary CTA = primary call-to-action (red button)
//secondary CTA = secondary call-to-action (blue button)
headerView.beginVerticalLayout(topMargin: 10)
.onTopOf(anotherView, spacing: 12, height: 24)
.onTopOfCTALayout(primaryCTA, secondaryCTA: secondaryCTA, spacing: 18)
.finishVerticalLayout(bottomMargin: 10)
Our expressive design language was fantastic. It made it easy to visualize views in our heads. We were slightly surprised to realize that we didn’t miss Interface Builder. (Another added benefit: it was possible to do all of our development in AppCode instead of Xcode!)
Moving to programmatic views was definitely the right choice for our team. We were able to reuse common elements, consolidate all of our style-related code in one place, and avoid the crashiness that plagued Interface Builder.
Last Thoughts
During this project, I learned a ton about the pros and cons of storyboards, .xibs, and programmatic views. I used to be .xib diehard, but I’m now convinced that there’s no perfect solution for all iOS projects.
Before starting your next project, I’d recommend thinking carefully about your goals before deciding on an approach for building your views. Are you building a throwaway prototype, or the foundation of a lasting project? Are you trying to meet a tight deadline? Will you need to reuse elements across many screens? Do you have clear visual design patterns yet? Your answers to these questions should guide your approach!