Wednesday, April 20, 2011

Using view controllers wisely

View controllers are fundamental objects in the iOS world. As a programmer, you create view controllers all the time, but have you ever thought about how to use them wisely? View controller themselves can be quite fat objects (especially their associated view), and using them sloppily can lead to a waste of precious resources.

In this article, I discuss issues related to how view controllers are displayed and managed, and solutions to (hopefully) make your life easier as an iOS programmer.

View controller containers

View controller containers are objects whose responsibility is to manage some set of view controllers, displaying or hiding them as required. Often, containers are view controllers themselves (UITabBarController, UINavigationController or UISplitViewController, for example), but this is is by no means required (UIPopoverController, for example, directly inherits from NSObject).

Writing custom view controllers is not an easy task and requires an entire dedicated post. I here only briefly mention two important properties that well-written view controller containers must exhibit:

  • Containers must correctly forward lifecycle and rotation events to the view controllers they manage. There is namely no magic free mechanism propagating those events from the container to its contents, the container implementation is solely responsible of making it right. A badly implemented container must be avoided at any cost since it is poisonous. It namely requires the view controllers it manages to implement its non-standard lifecycle, making them very difficult to reuse elsewhere
  • Containers must retain the view controllers they manage. This is consistent with existing built-in UIKit containers, and it would be really bad if this was not the case. This would put more burden on clients which, after all, cannot in general know when the container is done with its contents

  • UIKit view controller containers of course satisfy the above requirements. Be especially careful when using containers from other sources, though. In particular check that the above two properties are satisfied. If this is not the case, I strongly advise you not to use them.

    Displaying view controllers

    No matter what you intend to do, any application you write most probably begins with a root view controller. This is the view controller whose view is directly added as first subview of the main window. This view controller is not retained anywhere, you must therefore do it yourself (e.g. in your application delegate). Apple engineers took care of correctly sending lifecycle and rotation events to the root view controller of an application (otherwise we would obviously have had a problem). This view controller is therefore the starting point of your view controller hierarchy.

    When displaying a non-root view controller, several options are available:

  • If you have a view controller container at hand, you can use it to display your new view controller (e.g. as a new navigation level in a UINavigationController)
  • You can cover an existing view controller modally with the new one using +[UIViewController presentModalViewController:animated:]
  • You can display the new view controller by adding its view as subview of an existing view controller's view
  • You can display the new view controller by adding its view as subview of the main window (above the already added root view controller's view)

  • The third option is extremely dangerous and must be avoided, except if you are a container implementer. You namely become entirely responsible of forwarding lifecycle and rotation events correctly, which is very difficult to get right. You definitely do not want to face such issues if you are not implementing library or framework components. Of course, you can still get your application to work this way, but the code is likely to get ugly or to be a nightmare to understand. Usually, you namely end up putting all stuff into the viewDidLoad method since the other lifecycle methods cannot be trusted. That's bad. Don't do it. Period.

    The fourth option is tempting when you need to display a view controller quasi-modally (i.e. with the covered view controller still visible). It is dangerous as well, though: Rotation events are sent correctly to the first subview of the window, but not to subsequent ones. You therefore need to find some way to get these events to be sent to view controllers you display quasi-modally, otherwise they will not rotate properly. Moreover, you need to retain the view controller somewhere (as for the root view controller, most probably in your application delegate).

    You should therefore always use well-written view controller containers or +[UIViewController presentModalViewController:animated:] to display view controllers. This guarantees that you can trust view controller lifecycle and rotation events to occur as expected. The only exception to this rule is the root view controller of your application, which you directly add as first subview of your application window.

    You must retain the root view controller of your application, but what about the other view controllers in your application? Should you retain them as well? Let us discuss this issue now.

    Managing view controllers

    For convenience, we often need to keep references to view controllers. Since the first view controller of an application has to be retained, it can be pretty tempting to think this rule can be applied everywhere as well. This is especially appealing for novice iOS programmers since it makes them confident they will never access dead view controllers.

    But this is wrong. Keeping strong references to view controllers should be the exception, not the rule. Retaining view controllers where not necessary is most of the time not only useless, but also a serious waste of resources. While this can make sense in very special cases I will discuss below, if you need to keep a reference to a view controller (except the root one), a weak one suffices. As said in the previous section, you should namely always present view controllers modally or using well-behaved containers, and in all such cases the view controller you display will be retained for you. Over-retaining a view controller (and its associated view) is the easiest way to keep it in memory even if you are not using it anymore.

    There is only one reason you may sometimes need to keep several additional strong references to view controllers: Caching. A container probably releases a view controller it manages when it is done with it, but sometimes you might be pretty confident that the view controller will be used again very soon. In such cases, and especially if the associated view is costly to create, it might make sense to keep the view controller alive even after the container is done with it.

    Once you introduce such a cache mechanism, though, you become responsible of releasing these cached objects when memory gets critically low. If the view controllers strong references are kept by another "owner" view controller, you should take the following measures:

  • In the viewDidUnload method of the owner view controller, set the view property of every cached view controller to nil, and forward the viewDidUnload event to them afterwards
  • In the didReceiveMemoryWarning method of the owner view controller, set the view property of every currently invisible view controller to nil, and forward the viewDidUnload event to them afterwards. You can even release the view controllers themselves if you want, but be sure to implement some lazy creation mechanism so that the view controllers can be created again when needed

  • If the view controllers are not cached by a view controller, listen to memory warning notifications (UIApplicationDidReceiveMemoryWarningNotification) and, in your notification callback, proceed as for didReceiveMemoryWarning above.

    Conclusion

    Always use high-quality view controller containers or +[UIViewController presentModalViewController:animated:] to display view controllers in your application. If you need to keep a reference to a view controller somewhere, use a weak reference, except if you really want it to stay alive longer for caching purposes. In such cases, be sure to correctly respond to low-memory conditions.

    There is one exception to these rules: The first view controller of your application, which you directly add as first window subview, and which you must retain.

    In an upcoming article, I will thoroughly discuss the problems we face when implementing a view custom controller container. Stay tuned.

    Remark

    Starting with iOS 4, UIWindow has a new rootViewController property you can use to set... the root view controller of your application. Can you guess which semantics this property has been given without having a look at the UIWindow header files? (thanks 0xced for pointing this out)

    4 comments:

    1. Looking forward to the upcoming article on custom controller containers.

      There's little discussion elsewhere that isn't solely about replacing UINavigationController or UITabbarController.

      ReplyDelete
    2. I've been quite busy during the last weeks, but I will try to write this article ASAP.

      ReplyDelete
    3. I would really like to see that post about a custom controller container also....

      ReplyDelete
    4. This post is already available (http://subjective-objective-c.blogspot.com/2011/08/writing-high-quality-view-controller.html)

      ReplyDelete