Friday, January 13, 2012

UIStoryboard Best Practices

UIStoryboard is a hot topic right now in iOS. However, there have been many misconceptions on the topic. The first reaction to storyboards (what I have seen anyway) is that they are a panacea for all situations. Following that reaction I saw many people claim that storyboards are awful and broken. The truth is, both these ideas are wrong. During WWDC 2011 Apple may have over-promoted storyboards, thus giving the wrong impression and leading to unmet expectations. Let me correct this now: UIStoryboard is not an all-purpose solution, it is another tool for your toolbox.

My intent here is to provide the best practices for UIStoryboard. I will list my lessons learned, but there are probably more yet to learn. I expect over time that this list will expand to include other best practices for UIStoryboard.

Break your storyboard into modules

The first mistake developers have been making with storyboarding is producing large, monolithic storyboards. A single storyboard may be suitable for small apps but certainly not for large apps. Don't forget one of the key principles of programming and abstraction: decomposition. We decompose our code into modules to make them more reusable and reduce maintenance costs. The same is true for user interfaces, and it applies whether you are using UIStoryboards, XIBs, or anything else. There is an even deeper principle wanting to emerge: when code and user interfaces are decomposed together, then you have truly useful and reusable code, user interfaces, and products (you can quote me on this).

The first question to ask is "how do I identify the natural modules of my app?" Let's start with something easy. Many apps are based on a tab bar controller. Each tab is a natural module and you hence you can put them in their own storyboard. Next, if you have two tabs (or any two views for that matter) with a segue to a common view, then that common view (and its following hierarchy) is almost certainly another natural module. Once you have pruned your view hierarchy, you will probably find that each module can be given a name. For example, you may have your login storyboard, settings storyboard, about storyboard, main storyboard, etc. If you can give a fitting name to each module, then you have decomposed your hierarchy well.

Keep in mind that having a single view in a storyboard is not a bad thing. This is particularly true with table views. The power of static and prototype table view cells is very useful.

You should also note that breaking storyboards into modules makes them more friendly to version control and sharing among a team.

You don't need to convert everything at once

When you first saw storyboards, you may have had an urge to convert everything over to UIStoryboard. I know I did. Be aware that this does take time and you may not gain any immediate benefits from switching everything. Fortunately, you don't need to do it all at once. You can switch over the parts of your app that will benefit the most now. Then over time you can slowly switch out others.

Make custom UITableViewCell subclasses for prototype cells

Probably the best feature of UIStoryboard is prototype cells. They make custom table view cells easy. Prototype cells can be easily customized; however, the UITableViewCell class may not specify the IBOutlets you need. A custom subclass also gives you another advantage. You can create a -setupWith<#Object#>: method. This takes your setup code out of your table view controller and into the cell itself. This is where you can gain some great reuse.

Not only can you create custom UITableViewCell subclasses, but you can have one subclass service many prototype cells. For example, you may have a cell that takes a Person object. In one table view your cell may have main label on the left and a detail label on the right. A different table view may use the same cell but have a main label above and a detail label below. Your cell subclass handles the content, but the prototype cell handles the look and feel. Again we gain great reuse and flexibility by decomposing code and views together.

Don't forget about IBAction, IBOutlet, and IBOutletCollection

Segues are nice and can do a lot, but don't forget about the other IB fundamentals. Segues can't cross storyboards, but IBActions can. IBActions can also clean up an unnecessary spaghetti segue mess and reduce the need for custom segues. Everything has their place and their use; they are all tools at your disposal.

While on the topic of IBActions and family, I should cover encapsulation. Most, if not all, your actions and outlets do not need to be public. So, rather than put them in your public headers, put them in a class extension. See here for more details. Xcode/Interface Builder didn't handle this well in the past but now there are no problems. This keeps your interfaces much cleaner and easier for others to understand.

Don't forget about XIBs

Even though storyboards are very similar to XIBs, they still have a place. Simple, one-view modules may be better represented as XIBs. Also, XIBs can hold custom views without an associated view controller. UIStoryboard requires that view controllers be the basic unit.

You don't need to have a main storyboard

You may have noticed your app settings now has a "Main Storyboard" setting. To use UIStoryboard you are not required to use this. In fact, my latest project has neither a main storyboard nor a main XIB. It uses a programmatic UITabBarController. From there I load all my tab storyboards. The beauty of it all is that it doesn't matter whether I use UIStoryboards, XIBs, or code. All three options are flexible enough to work with each other in any combination.

Specify the name of your app delegate in main.m

As I mentioned the other day, you may need to change your main.m file if it doesn't specify your app delegate's name. It should look something like this:

UIApplicationMain(argc, argv, nil, NSStringFromClass([MyAppDelegate class]))

Put your storyboards in a category

To get one of your storyboards, you need to call +storyboardWithName:bundle:. Be wary any time you need to hard code a string for a key, especially one that can change frequently. Should you ever need to rename your storyboard, you will have to change all references to that storyboard. My favorite way to handle this is with a category. It looks something like this:

@implementation UIStoryboard (MyApp)

+ (UIStoryboard *)mainStoryboard {
    return [UIStoryboard storyboardWithName:@"MainStoryboard" bundle:nil];
}

@end

The benefit of the category approach is that if you ever rename a storyboard, you only need to change the hard coded string in one place. It also makes your code more readable.

Summary

If you take anything from this post you should remember these:
  1. Decompose your code and your views.
  2. UIStoryboard is a great tool, but it's not the only tool.
I hope these tips and best practices help promote proper use of UIStoryboard. I invite all my readers to let me know of other best practices they encounter.