Storyboards seem to be a big point of contention in iOS development. Some see them as wonderful additions, some as a poorly designed and pointless hindrance that Apple seems intent on force feeding us. There is one thing that’s consistent though: almost nobody is using them right.
That’s a bold statement to make. It's based on the many conversations I've had with people, and the many tweets and blog posts on the issue. One of the key things I see is a rather innocuous question: NIBs or Storyboards? This highlights a massive misunderstanding of what Storyboards offer us, as it pits them as a replacement to NIBs. In reality they can be easily used to complement NIBs.
In this post I want to show how I’ve been using Storyboards and NIBs, together with a few ideas I’ve been throwing around for structuring apps. It may not represent the best way of doing things, there are even some parts I’m still unsure of myself. Hopefully it will give you some insight in how to get more out of Storyboards and NIBs, or at least provide a starting point for debate.
I’ll be referencing a sample project that I’ve posted on github. This won’t be a step-by-step guide on how to build the app, but more a post highlighting interesting aspects.
The app is quite basic. It contains a list of entries and tapping on one takes you to a details screen. From this screen you can also few more details. There is also a “settings” screen, which actually just shows a similar list. This app allows me to show some very important concepts though.
The biggest problem I see with people using Storyboards, is they throw all the UI for their app into a single Storyboard. This is equivalent to throwing all the code for the same app into a single file. It becomes hard to understand, hard to use and almost impossible to work with on a team. If we know not to do this with code, why do we do it with Storyboards?
It’s partly Apple’s fault. They haven’t really shown us how to programatically interact with a storyboard, instead focussing on how you can easily add scenes and segues. They’ve also promoted the single Storyboard idea, though that may be a side effect of the simplicity of the projects they show. First we'll look at conceptual side of splitting up our app into several Storyboards, then in the next section we’ll see how we can hook things up with code.
We want to try and find clean breaks in our app, where we can separate out functionality. A good sign for this is where we do some significant context-changing transition, such as a modal transition. If we take the camera app, we have two contexts: taking photos and viewing/editing photos. In iBooks we have browsing books, we have viewing a book and we have viewing the iBooks Store. In the sample app we have the Settings and the Entries context.
Even though there is some degree of hierarchy linking these, it is much weaker than the hierarchy linking the views within them. When we see these contexts, it’s a sign that we can split things up into a separate Storyboard. It’s rare that we work on UI impacting features that span contexts, so by dividing our Storyboards along these lines we make it easier for us to focus, as all the irrelevant stuff is hidden away in another file. It also reduces the risk of merge conflicts, especially on smaller teams where it’s more likely you’ll only have a single person working on any one context at any time.
So we’ve got our separate Storyboards and we’ve linked the scenes within them, but how do we replace the segues we had before. We can’t link between Storyboards in Xcode. The answer comes in one of the simplest classes in UIKit: UIStoryboard. It only contains 3 methods. The first +storyboardWithName:bundle: creates a Storyboard object from file with the supplied name. The other two allow us to create view controllers from our Storyboard.
A Storyboard gives us two ways to reference a view controller. The first is to assign it as the initial view controller. This is shown in the editor as an arrow pointing from nothing towards a scene. The other way is to give our scenes identifiers. These are strings that we can pass into -instantiateViewControllerWithIdentifier: to generate a view controller object from that scene.
There is one more useful method that was added to UIViewController, which allows for some really powerful concepts. It is the storyboard property. This holds a reference to the UIStoryboard object that represents the Storyboard the view controller was created from. As we usually have a single root object in a Storyboard (having multiple is a sign that maybe you need to split it up), this means we only need to create a UIStoryboard object once.
We’re going to take the reins from UIKit and handle setting up the initial Storyboard ourselves. We’ll see this in two places. The first is -[M3AppController launchInWindow:], where we set the root view controller of a window and make that window visible. There is nothing really different here to what you may have seen before (except that it’s usually found in the app delegate, but we’ll explain why it’s here later in the post).
You’ll see that the root view controller is coming from -[M3AppFactory entriesNavigationController]. This is just a lazy loading getter, but it is doing its loading in a slightly different way. We’re not calling alloc/init on a class. Instead we’re creating a storyboard (line 26) and setting our view controller to be the initial view controller (line 27), in this case the navigation controller for our entries context.
We can also transition to another scene quite easily. The simplest example is in -[M3EntryDetailsViewController showMoreDetails:]. All we do here is instantiate our view controller, set the data object on it, then push it onto our navigation controller.
You may be asking why I bothered using Storyboards rather than just instantiating the class itself. The reason is loosely coupled code is good and Storyboards help us to achieve that. I could potentially give a different scene the “moreDetails” name. As long as this scene also responds to -setEntry: then the code will work as is. I’m putting the details of the high level app flow from my view controllers and into the Storyboard. I could even potentially completely swap out the Storyboard, as the class only references the storyboard property than any specific Storyboard.
If you have looked at Entries.storyboard, you will have noticed that I have connected my scenes up with Segues, but I haven’t actually used them for any of my transitions. I just want to cover the pros and cons of Segues and why I’m currently doing them the way I am.
The big problem with Segues, is they force you to funnel all your logic through a single method, -prepareForSegue:. It is very much the same problem faced with KVO. You end up with a method that has a series of if statements that work out the correct code to call based on a context. It feels awkward and not very OOP. This is why I tend to avoid them, instead favouring handling the transitions myself. If they operated in more of a target action system I’d be much more inclined to use Segues.
There are good reasons to use Segues though. The method we’ve been looking at makes some assumptions about the app structure. It assumes this view controller will be displayed in a navigation controller and that the more details view controller should also be shown in that navigation controller. Just as Storyboards allow us to pull the details of *what* view controller we’re moving to, Segues allow us to pull out the details of *how* we move to that controller.
So why connect my scenes up with Segues in the Storyboard if I’m not going to use them? Partly because my Storyboard act as a visual representation of the flow through the app and the arrows help with that, but mostly because it makes configuring navigation items easier in the editor if you hook them up.
You may be wondering how NIBs fit into this world. For all the benefits of Storyboards in structuring our apps, they kind of suck at layout. Sometimes you have views where you have a complex layout and/or one that doesn’t neatly fit the confines of a full screen view controller. You could handle it all in code, but that’s a lot of writing, testing and debugging that needs doing. It’s always been far simpler to lay it out in a NIB.
As with UIStoryboard, a lot of developers aren’t fully aware of how to interact with NIBs in code. It’s understandable, as we often have it handled for us by the likes of UIViewController. The main class we use for working with NIBs is the UINib class. Like UIStoryboard, it is incredibly simple, with two methods for creating a UINib object and one for unpacking its objects.
We use a NIB in the sample project for the details screen. This is a scroll view that needs to contain a layout. In this case the layout is quite simple, but if we had a more complex data structure it could require a more complex view to display. We lay this out in EntryDetailsContentView.xib. This contains a view of type M3EntryDetailsContentView and has a File’s Owner of type M3EntryDetailsViewController. However, instead of making the view in the NIB the view controller’s view, we have hooked it up to the contentView outlet.
The magic is done in M3EntryDetailsViewController.m on lines 25-26. Here, we load the NIB and then we instantiate it, passing ourself in as the owner. What this does is load the NIB and set up all the outlets. By passing ourself in as the owner, all the outlets and actions connected to the File’s Owner in the NIB are now connected to us. This means that our contentView property is now filled. We then just add the content view to the scroll view and set up its constraints.
We’ve spent some time splitting up our app and nicely partitioning things. Unfortunately, just as two objects may want to talk, two contexts within the app may want to talk (we can almost think of each partitioned context as an object in its own right). There are already various solutions we could try. We could pass lots of references around so the objects can talk directly. We could fire off notifications. We could throw it all in the app delegate and call [UIApplication sharedApplication].delegate.
I’ve started doing something that’s a bit of a combination of these, which offers less housekeeping than passing lots of objects around, less indirection than notifications (which means more help from Xcode) and less ickiness than throwing it all in the app delegate. The solution is two classes: M3AppController and M3AppFactory.
M3AppFactory is rather simple. It creates all the core objects for the app. In a simple app like this I throw all the creation in here, but in more complex apps I’ve created separate factories for the frontend and backend (the former will use the latter). You could also split up the factories by function if you wished. The idea though is to have a central location which all the key longterm objects are created.
M3AppController directs all the flow in the app. At times I’ve felt it risks becoming a god object. It can end up controlling the display of each context, as well as various app level functions such as sending analytics or presenting messages, leading it to grow rather large in line count. However, it still maintains its purpose of managing the flow between the different contexts of the app.
Rather than a god object, it feels a bit more like a CEO object. The CEO is in overall control of the company and oversees various departments (contexts). The CEO needs to orchestrate the interaction between these departments in order to get the best out of them.
The key point of the App Controller is that it is passed into all the main objects in the app (this is done in the App Factory, and is a key reason to have that class), so it can be accessed via a property. We could create a +sharedController method, but the property way of working allows for looser coupling and easier testability.
Lets look at how we manage the main contexts in the app. We saw earlier that we set up the main Entries context in -[M3AppController launchInWindow:]. This is in here, rather than in the app delegate, because we want to keep the app delegate lean. It should focus purely on handling the delegate methods of UIApplication.
We treat the Entries context as sort of a master context from which all other contexts appear, but this is purely due to how this app is structured. Because of this, we don’t have any means to show or hide the context beyond launching the app, as all other contexts will appear on top of it.
The other context is the Setting context. Here we have methods to both show and hide the settings. If you look at -[M3AppController showSettings] you’ll see we’re simply presenting the settings controller on the entries controller. The hide method below just dismisses it. These methods are called from inside the contexts. The show method is called from -[M3EntriesViewController showSettings:], where in response to an action being invoked from the UI, we get our App Controller object and tell it to showSettings. Similarly in -[M3SettingsViewController done:] we call hideSettings.
The Settings context isn’t really for dealing with settings. In fact it mostly emulates the Entries context by display a list of entries. If you tap on one of these, it will display that entry and then hide the settings screen.
Displaying an entry is an interesting case, as it requires one context to control a transition within another context (rather than just transitioning between contexts). In -[M3SettingsViewController tableView:didSelectRowAtIndexPath:] we find the relevant M3Entry object, then tell the app controller to show details for that entry and hide the settings. It’s very simple to read and use (more so than passing round a navigation controller, creating the relevant view controller and pushing it on, all from the settings view controller).
Looking at -[M3AppController showDetailsForEntry:] we can see there’s nothing complex about this transition. We are asking the factory for a details view controller for this entry (which simply instantiates a details view controller from the Storyboard) and pushing it onto the entries navigation controller.
If you’ve only got one thing from this post, I hope it’s the knowledge of how to work with Storyboards and NIBs in code. My main aim was to get people breaking up their god Storyboards into smaller, more manageable units.
Everything else is less concrete and certain. They are ideas I’ve been playing with and, while they’re far from perfect, they help make the flow of my app easier to understand and reason about, while also helping to separate each context out. These ideas grew out of my usage of multiple Storyboards, which made me think more about these contexts as distinct units. Maybe it will encourage you to try the same approach, or maybe build upon it to make something even better than I have now.