Don’t Nest Those UIViewControllers!

Like many iOS developers, I came to iOS from a non-Mac development background. Switching platforms always requires some time to adjust to the different libraries and usage patterns of the new platform, and Cocoa is no exception. I really like it now, and am very comfortable working with it, but there were some things it took a little while for me to use properly.

Nasty, Nested UIViewControllers

UIViewControllers and related classes are straightforward and pretty clear in their purpose. Apple’s user guide gives a good overview and is worth reviewing from time to time. But I have to admit that early on, I was consistently misusing custom UIViewControllers by trying to nest one within the other.

Like many beginners, my approach was wrong, but I managed to make it “work” anyway. I would usually create a new custom UIViewController subclass that managed a view and hacked together working behavior through liberal use of viewDidLoad and other methods like that. Nothing I’m proud of, or that I want to share with you — but I know I’m not alone, so I don’t feel too bad about it.

In principle what I wanted was a reusable view with a corresponding controller just for that view, my mistake was in assuming that the controller had to be an instance of UIViewController. Apple is quite clear in their documentation that there should be a single UIViewController that manages the overall view. Even as I twisted multiple, nested UIViewControllers to do my will, I knew it was a bad idea, but early on I was not sure how to do it better.

Simple Answers Are Sometimes Hard To See

When your mind is following wrong assumptions and thought processes, the truth can be hard to see.

The answer was actually quite simple. I can do exactly what I want with just a simple class for the view’s controller. I can even use Interface Builder if I choose to design the view and configure outlets and actions.

Part of my early error was because I wanted the iOS runtime to somehow magically know about my views and send them the same set of viewDidLoad, viewWillAppear, and related messages that UIViewControllers receive when used properly. I realized that trying to twist multiple UIViewController instances into behaving like the magic existed was foolish, so I found a better way.

New Pattern For Reusable Subviews

My current pattern is a simple approach that is an amalgam of various methods I learned from others. I use a single UIViewController to manage the overall view, and replace portions of the subview hierarchy as needed with individual views that have their own controller, which I call a view manager to avoid confusion. (After all, naming things is purported to be one of the two truly hard things in computer science.)

I have a standard set of ViewManagers that I use across projects, which derive from the same AbstractViewManager class and include a few standard behaviors. Each ViewManager instance is owned by a “real” UIViewController instance, and to help ViewManagers participate more fully in life cycle and memory events of UIViewControllers, the owning UIViewController instance forwards messages to its ViewManager instances as appropriate.

The accompanying example project has a split view controller at the root, with a list of sample data to display in the master table view. The simple detail is handled by DetailViewController which owns an instance of two ViewManagers: WebViewManager and ImageViewManager.

When a row is selected, the master table view controller sets the ‘detailItem’ property of DetailViewController, which expects a dictionary of values. The setter method for the detail item looks like this:

- (void)setDetailItem:(id)newDetailItem {
    [detailItem_ autorelease];
    detailItem_ = [newDetailItem retain];

    if (self.popoverController != nil) {
        [self.popoverController dismissPopoverAnimated:YES];
    }        

    NSString *detailType = [detailItem_ valueForKey:@"type"];
    if ([detailType isEqualToString:@"image"]) {
        self.currentViewManager = self.imageViewManager;
        [self.imageViewManager loadImage:[detailItem_ valueForKey:@"name"]
                                   title:[detailItem_ valueForKey:@"title"]];
    } else if ([detailType isEqualToString:@"web"]) {
        self.currentViewManager = self.webViewManager;
        [self.webViewManager loadURL:[detailItem_ valueForKey:@"url"]
                               title:[detailItem_ valueForKey:@"title"]];
    } else {
        self.currentViewManager = nil;
    }

}

This will set the currentViewManager property to the appropriate ViewManager instance and configure its view appropriately.

The custom setter for currentViewManager sends the appropriate life cycle events to both the old and new ViewManager:

- (void) setCurrentViewManager:(AbstractViewManager *)currentViewManager {
    if (currentViewManager == currentViewManager_) {
        return;
    }

    AbstractViewManager *oldViewManager = [currentViewManager_ autorelease];
    NSArray *subviews = [self.contentView subviews];
    
    if (oldViewManager) {
        [oldViewManager viewWillDisappear:YES];
    }
    for (id subview in subviews) {
        [subview removeFromSuperview];
    }
    if (oldViewManager) {
        [oldViewManager viewDidDisappear:YES];
    }
    
    currentViewManager.view.frame = self.contentView.bounds;
    if (currentViewManager) {
        [currentViewManager viewWillAppear:YES];
    }
    
    [self.contentView addSubview:currentViewManager.view];
    currentViewManager_ = [currentViewManager retain];    
    
    if (currentViewManager_) {
        [currentViewManager_ viewDidAppear:YES];
    }
}

Each ViewManager instance is lazy loaded, and unloads itself when the property is reset like this:

- (WebViewManager *) webViewManager {
    if (webViewManager_ == nil) {
        webViewManager_ = [[WebViewManager alloc] initWithNibName:@"WebViewManager" bundle:nil options:nil];
        [webViewManager_ viewDidLoad];
    }
    return webViewManager_;
}

- (void) setWebViewManager:(WebViewManager *) webViewManager {
    if (webViewManager_ == webViewManager) {
        return;
    }
    
    [webViewManager_ viewDidUnload];
    [webViewManager_ autorelease];
    webViewManager_ = [webViewManager retain];
}

DetailViewController also forwards ‘didReceiveMemoryWarning:’, ‘viewDidUnload:’, and other messages to instantiated ViewManagers when needed.

Conclusion

The ImageViewManager and WebViewManager classes are just simple examples of what can be done, but they should point you in the right direction.

I hope this will help someone else dig themselves out the nested UIViewController rathole that I fell into on my iOS development journey. If this sounds useful to you, download the demo project and give it a try.

Have a great day!


This is post 6 of 10 on my second iDevBlogADay run.

We all need the support of others to do our best work. Find other like-minded developers that will provide encouragement and motivation through local user groups, conferences or meetups. A great collection of indie iOS developers have helped me stay on track through meetups, 360iDev, twitter, and iDevBlogADay.

I regularly attend Cocoa/iPhone developer meetups in Cincinnati, Ohio and Columbus, Ohio. If you are in the central or southwest Ohio area, come join me at either monthly meetup:

If you depend on iOS development for your livelihood, or would like to get to that point — you really need to attend a conference dedicated to helping you get better, and I can think of no better conference for that purpose than 360iDev — you should register today!. Much of what I am able to do professionally is due to the things I learned and the people I met there.

Finally, here is a little more information about me, Doug Sjoquist, and how I came to my current place in life. You should follow me on twitter and subscribe to my blog. Have a great day!