Dec 13, 2011

UIStoryboard Issues

Background

In August I joined a new start-up as lead (and only) iOS Developer. I managed to convince the founders that targeting iOS 5 was a good idea and as such I’ve embraced a lot of new iOS 5 only technologies in the app including UIStoryboard.

After a few months of development with UIStoryboard I’ve decided that the technology just isn’t mature enough to be used in a complex iOS application and have now replaced all* UIStoryboard with traditional NIBs.

(* I have left in standalone storyboards for parts of the app heavily in flux, but will be replacing this usage with NIBs in the future).

Storyboard Editor

The storyboard editor in Xcode 4 combines all the problems of the Interface editor (e.g. broken keyboard navigation, somewhat broken selection semantics, amnesia for useful settings like “show bounds”, etc) with the inability to edit interfaces on a small (17”) screen. Once the storyboard expands beyond a half dozen or so view controllers the storyboard editor becomes very hard to use, with constant panning and zooming to find the view controller you want to edit.

One problem with storyboards with lots of view controllers (my app had around 30 view controllers in a single storyboard) is that everything looks the same. Visually a lot of view controllers end up being some kind of variation on a white rectangle (esp. when zoomed out). I found myself tending to rely on a knowledge of the storyboard “geography” to find the view controller I was looking for. And even familiar with the layout of the view controllers I still found myself hunting and pecking for the right view controller (“oh no wait, that’s the parent UINavigationController, I must want the child instead”).

A somewhat related but inverse problem is that it seems to be impossible to use UITableView static cells & prototype cells outside of a storyboard. This seems strange. Being forced to use storyboards if you want to use this very useful feature is serious problem. I have to assume this is an oversight.

Spaghetti” Segues

With moderately complex storyboards the view controllers and their segues begin to resemble spaghetti. I found this especially common when I had multiple “detail” view controllers that could be accessible from each other (think of a twitter app - you can view people in a people table and tweets in a tweet table, and drill down in people/tweet detail screens. But you can also access a person detail screen from an individual tweet detail screen and vice versa). I wonder if the master view controller source list (presented on the left of storyboard editor) should not be the primary view for the editor, with the ability to focus on one view controller at a time?

Segue Identifiers

It becomes readily apparent that the segue identifier field (settable in the editor) is the best way to differentiate between segues in large prepareForSegue: implementations (see more on this later). It would be very handy if Xcode 4 supported a “warn on missing segue identifier” option when storyboards are “compiled” into individual NIBs. (feature request)

View Controller Identifiers

It is possible to assign view controllers in a storyboard an identifier. Unfortunately this identifier property is not exposed in the UIViewController class. This makes it very hard to perform safe introspection of the view controller hierarchy at runtime. It would be really nice if identifier was exposed for view controllers as well as for segues. (Feature request)

Push-only Segues

Segues are by default “additive” only: you can push a view controller onto the navigation stack or present it modally but it isn’t possible to perform other common navigation tasks without resorting to custom segues. For instance you can’t jump to a view controller further down the navigation stack or change the tab of a tab bar. And the ability to jump to an arbitrary view controller in the view controller hierarchy (without a push) doesn’t seem possible at all.

Custom segues do allow you to do some of this. In fact I have written a custom segue to jump to a different tab in UITabBarController, but it was rather hacky code to get around the fact that the segue automatically created a new destination view controller for me.

I think it would be handy if these, more complex segues, were built into the UIStoryboard system by default.

Misuse sender field.

A common iOS design pattern is to create a new detail view controller, populate it with the relevant detail and then present it (either push onto the navigation stack or present modally). Generally I’d accomplish this with a custom “initWithFoo:” method on the detail view controller.

CUser *theUser = [self.fetchedResultsController objectAtIndexPath:indexPath];
CUserProfileViewController *theViewController = [[CUserProfileViewController alloc] initWithUser:theUser];
UINavigationController *theNavigationController = [[UINavigationController alloc] initWithRootController:theViewController];
[self presentViewController:theNavigationController animated:YES completion:NULL];

With storyboarding there’s no clear way to pass data to a detail view controller. The best I could come up with was by abusing the sender parameter to pass the data the detail view controller needs, e.g.:

CUser *theUser = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self performSegueWithIdentifier:@"ID_USER_DETAIL" sender:theUser];

The performWithSegue: method would then take the data and hand it to the already created destination view controller:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
    {
    if ([segue.identifier isEqualToString:@"ID_USER_DETAIL"])
        {
        UINavigationController *theNavigationController = AssertCast_(UINavigationController, segue.destinationViewController);
        CUserProfileViewController *theViewController = AssertCast_(CUserProfileViewController, theNavigationController.topViewController);
        theViewController.user = sender;
        }
    }

(Note I use my AssertCast_ macro to make sure my assumptions about the view hierarchy are correct, I’ve been bitten too many times in the past by bad view controller hierarchy assumptions to not be paranoid here)

Not only is this a lot of code and complexity to replace something that was rather simple before, it seems to be an abuse of the “sender” parameter. Sender should really be (at least according to tradition) the button or the table cell or the map annotation, etc that triggered the segue and not the data the destination view controller needs to configure itself.

I tried to build my own performSegueWithIdentifier:sender:userInfo: and prepareForSegue:sender:userInfo: methods in a UIViewController category but it was clear it was going to have to do some nasty things (like define a prepareForSegue:sender: in the category in question) and I decided it was a no-go.

Perhaps I’m missing something here and there is a better way to accomplish this.

Bloated prepareForSegue:

My root view controller has become a source view controller for lot of segues and therefore its prepareForSegue: has become a stupidly large method filled with a lot of “if (segue.identifier isEqualToString:@”…”)” statements in a row. I don’t like this. I much preferred the way the same code was distributed to many view controllers instead.

I find this problem causes the code to become more brittle and less flexible. If I change the view controller hierarchy I need to find the relevant block of code in the source view controller and move it to the new view controller. Previously I didn’t have to do this at all. This totally eliminates the flexibility that storyboards can provide.

No user defined relationships

This was really the nail in coffin for storyboards for me.

I needed to create a new container view controller type that could flip between multiple child view controllers (rather like a UITabBarController but with different UI and animation). I implemented the view controller with a child “viewControllers” property and got the transitions and UI working and then realised that I couldn’t wire it up in a UIStoryboard successfully. I quickly realised that only UINavigationController and UITabBarController could have non-segue relationships to other view controllers (*). This basically made it impossible to use one large storyboard and would need to use traditional techniques (static view controller instantiation in this case) and break my single monolithic storyboards up into smaller storyboards that I could load root view controllers from as needed. Unfortunately because of the spaghetti problem I discussed previously this was going to be ugly.

At this point, combined with the other problems and issues I’d encountered up to now I decided to rip out my storyboard and replace it with the traditional one (or more) NIB per view controller.

No code size benefits

I spent almost a day replacing my storyboard (and related code) with NIBs (I am sure I have introduced new bugs into the app in the process that will take further time to find and fix). In doing so I actually reduced the line count by about 400 lines of code. It’s pretty clear that storyboards wont reduce the total of number of lines code in the app and due to the problems with prepareForSegue: discussed previously I don’t think storyboards will allow you to iterate designs more quickly than using NIBs.

Is this my fault?

So I’m left wondering if this could my fault, that I’m missing something or just don’t understand how I should be using storyboards. I don’t think so. I do think that I could learn to stop worrying about the sender/userInfo issue and perhaps I could work out a smart way to solve the prepareForSegue: bloat problem. But really, I don’t have the time for this, I need to be able to implement UI in a speedy manner without having to solve the “how do I make this work with UIStoryboards” problem.

Summary

I do think the UIStoryboard technology has a lot of potential but as I’ve discussed there are some rather severe issues with the current implementation that make it burdensome to use. That said I look forward to further iterations of the technology. Also if anyone has worked around these problems or think I’m doing it wrong please let me know.