In
a previous article, I mentioned high-quality view controller containers as an essential tool to help you manage your view controller hierarchy efficiently. Writing such objects is difficult to get right, though, since many issues have to be faced simultaneously.
In this article, we will define which requirements view controller containers should fulfill and, of course, how they can be implemented. This should not only provide you with hints about containers themselves, but also about view controllers in general.
To my great surprise, though view controllers are part of the everyday life of an iOS programmer, I discovered they are often misused and abused, probably due to a lack of understanding. Programmers often have a way to quickly judge the quality of some code base, mine for iOS projects is to look at how its view controllers are implemented. Implementations paying attention to details like memory exhaustion surely deserve more trust and interest than implementations barely tweaking the UIViewController Xcode template.
Before I start, please apologize for the length of this article. There is so much to be said about containers and view controllers in general that I thought the subject deserved an extensive treatment. I humbly hope this article will help demystifying this class and make you a more proficient iOS programmer.
Examples of view controller containers are available from the
CoconutKit library. You may want to have a look at their implementations after you have read this article.
A definition
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).
A remark about naming: When speaking about container objects in general, I use the term "view controller container", or simply "container". When speaking about containers which themselves inherit from UIViewController, I specifically use the term "container view controller". Please keep this distinction in mind.
Before delving into specifics, let me introduce a practical example of a container object.
Practical example
One common problem with view controllers is that they tend to become quite fat as a project evolves. You start with a small, clean view controller princess, and you end up with a clumsy, cluttered toad superimposing many views, powered up by thousands of lines of code barely sustainable.
In one iPad project I worked on for a bank, we had to display a detailed view for a fund. This view was made of three sections, one for a performance graph, another one for various facts about the fund (and made of several table views), and the last one for documents. We knew from the beginning that other sections would be added in the future.
Instead of stuffing everything into a single view controller, we split up our content into three view controllers: Graph data, fund facts and documents. Those view controllers were then embedded into another view controller containing some general fund information and a UISegmentedControl to toggle between sections. The result can be represented as follows, where the blue rectangle corresponds to the area where the embedded view controllers are drawn:
This is in fact a common situation I face when writing iPad applications: I often need to embed a single view controller into another one. And since I face this problem so many times, I decided to write a custom container object, HLSPlaceholderViewController, to make my life easier. Here are the requirements I decided my container implementation had to fulfill:
The container is itself a view controller, from which programmers must inherit to create their own
The container view controller must be customizable, either using a xib or programmatically. There must be a way to define a placeholder area where the embedded view controller's view will be drawn
Only one embedded view controller can be displayed at a time. The current view controller can be swapped with another one simply by assigning a new view controller to a property
Built-in transition effects similar to those usually encountered on iOS must be available when changing the embedded view controller (push, fade, etc.)
The view controller displayed in portrait orientation might be completely different from the one displayed in landscape orientation. Some mechanism should be available to swap view controllers transparently during rotation
An HLSPlaceholderViewController container can be used to compose more sophisticated interfaces. Here is an example where one is used to create some kind of split view controller. The HLSPlaceholderViewController view contains a table view on the left and a placeholder area on the right (blue rectangle), where embedded view controllers are drawn. When clicking on an item on the left, a corresponding view controller is displayed on the right:
The requirements listed above pretty much covers every aspect I usually expect from a view controller container. While not every view controller container requires this customization level, all must exhibit common properties we discuss in the next section.
View controller container properties
A view controller must at least fulfill two major requirements which I already mentioned in one of my previous articles about view controllers:
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. Otherwise programmers using a container would have to retain and release the view controllers as they are needed by the container. This would put more burden on programmers who, after all, cannot in general know when the container is done with its contents (it would not really make sense to have delegate methods called for this purpose, wouldn't it?)
Containers must correctly forward lifecycle and rotation events to the view controllers they manage
While the first requirement should be obvious, the second one deserves more attention since it is a tricky part in getting a container implementation right.
The view controller lifecycle
If I were to give awards to the most important documentation pages an iOS developer must read, the
View Controller Programming Guide for iOS would certainly be among my favorite ones. Before iOS 5, this guide contained so much information (especially in the
Custom View Controllers section) it could be quite overwhelming at first. With the new iOS 5 storyboard, the guide has been updated and is now sadly far less comprehensive that it was. Any iOS developer, though, and any container implementer in particular, should still read this guide more than once, though, because it is the only way to understand some of the subtleties associated with view controllers.
The iOS documentation and the UIViewController header file define the lifecycle of the view associated with a view controller. Let me recall it and pinpoint its numerous pitfalls:
A view controller's view does not exist until you access the UIViewController's view property. The first time you access it, the view gets lazily created, either by calling a loadView method if one has been implemented, or by deserializing the contents from a xib (I prefer using xibs most of the time since they make my life far easier when designing views). If you need to test whether a view controller's view has been created, you must call the isViewLoaded method of UIViewController. You cannot of course just test the view property since this would trigger lazy view creation!
After the view has been created, the viewDidLoad method is called. When implementing it, and if your view was created from a xib, you can use the outlets bound using Interface Builder to further customize your view programmatically. Be careful that the view dimensions are not final yet: They are the raw dimensions obtained right after the view has been loaded, but these will probably still be adjusted to take into account other screen elements like the status bar or a navigation bar. You should therefore be careful not to perform any view frame adjustments relying on your final application frame in the viewDidLoad method (since the final frame is not known yet). In general, the viewDidLoad method is a perfect place for skinning purposes (colors, fonts), setting localized strings or performing the initial synchronization between model data and view
The viewWillAppear: method is called. At this point, the final application frame is known and you can perform view size adjustments. The viewWillAppear method is called before any animation displaying the view begins (whether the view has already been added as subview or not is irrelevant as far as I know)
The viewDidAppear: method is called after the animation displaying the view finishes
These are the events related to view creation and display. Two additional events occur when the view disappears:
When the view is about to disappear, the viewWillDisappear: method is called. This happens before any animation hiding the view starts
After the animation hiding the view finishes, the viewDidDisappear: is called. Note that the view does not need to be removed from its superview or released when it has disappeared
Moreover, if a memory warning is encountered during the time the view is loaded (whether it is actually visible or not), the UIViewController didReceiveMemoryWarning method is called. If the view controller's view is invisible, this method takes care of setting it to nil, calling the viewDidUnload method afterwards. To free as much resources as possible, the viewDidUnload method is therefore where you should set your view controller's outlets (and related view resources) to nil.
In essence, the view lifecycle above is a formal contract:
When implementing a view controller, it gives you guidelines and hints about where you should insert your code so that the view controller displays correctly, efficiently, and properly responds to low-memory conditions
When implementing a container, it defines the behavior you must implement so that users of your container can rely on the usual view lifecycle to occur as expected. It would have been nice if this was guaranteed automagically, and it is (to some degree) if you are using the new iOS 5 containment API. But in general you still have to do the hard work
If a view controller container fails to implement the view lifecycle correctly (i.e. if it implements its own non-standard lifecycle), it gets poisonous and should not be used. Imagine you are implementing a view controller you want to display using such a container, and you discover that your viewWillAppear: event is not fired correctly. What can you do? If you are lucky you might be able to stuff everything in viewDidAppear: (if it gets called correctly, which I would not count on). If you are not lucky you might end up having to stuff everything in the viewDidLoad method, which is far from optimal since at that time view dimensions are not reliable (well, if the container implementation is *really* bad, they might be, making things even worse). Basically, the result you get is a view controller tightly bound to a container's implementation, not to the view lifecycle contract. Such objects are most probably incompatible with other view controller containers, and if you later need to refactor your application to use other containers, you will probably have to move code around to get it to work.
View controllers are also governed by a contract for orientation changes. We discuss it in the next section.
Handling rotation
As for the view lifecycle, rotation events are precisely defined as well. Two kinds of rotations are available:
two-step rotation, which generates events for the two halves of the rotation process
one-step rotation, which treats the rotation as a whole
One-step and two-step rotations cannot coexist in a same view controller implementation. If one-step rotation methods are discovered, two-step rotation methods are ignored. But this is not a severe issue since two-step rotation is deprecated (and even formally starting with iOS 5). I strongly advise you to forget about its existence altogether, and I will only discuss one-step rotation in the following.
For one-step rotation, four methods are relevant:
When the interface orientation changes as a result of the user rotating the device, the shouldAutorotateToInterfaceOrientation: method is called to know if the view controller supports the new orientation
If the answer is YES, willRotateToInterfaceOrientation:duration: is called
The willAnimateRotationToInterfaceOrientation:duration: method is called just before the rotation animation starts. At this point, the final frame after rotation is known, which makes it a perfect place for frame adjustments
When the rotation animation ends, didRotateFromInterfaceOrientation: is called
Besides those methods which get called for you when rotation occurs, the UIViewController interface offers an interfaceOrientation method which you can call to get the current interface orientation. The problem with this method is that it is only reliable when the view controller is displayed in a standard way (i.e. using a built-in UIKit view controller container, calling a method for modal display, or when a view controller is added as first subview of the application main window). There is a way to solve this issue, though, but we will discuss it later.
Until now, we have discussed documented properties of a view controller. Other common traits arise when using view controllers, both for convenience or efficiency reasons. We discuss them in the next section.
Informal properties of view controllers
Besides lifecycle and rotation events, view controllers embedded into containers exhibit some common properties:
When a view controller gets inserted into a container, it should be able to know which container it was inserted in (as with the UIViewController navigationController or tabBarController properties, for example). Moreover, UIKit built-in containers prevent simultaneous insertion of the same view controller into several containers, a behavior we should expect for custom containers as well
A view controller's view should be instantiated when it is really required to avoid wasting resources prematurely
When in a container, user interaction is usually restricted to the view controller on top. Even if other view controllers stay visible underneath (e.g. through transparency), interaction with them is usually not desired
View controller properties for use in a navigation controller, e.g. navigation elements and title, might need to be forwarded through a custom container for which the view controller is currently the active one. This way, when itself embedded into a navigation controller, the container view controller is able to display the navigation elements of the currently active child view controller. Forwarding navigation elements through the container does not always makes sense, though, this behavior must therefore be optional. In the example below, where forwarding has been enabled, the title of the embedded view controller (in blue) becomes the one of its container, and is thus displayed in the navigation bar:
Usually, when you hand over a view controller to a container, you do not keep any additional reference to it since it is already retained by the container. When it gets removed from the container, and since no additional strong references exist, it simply gets deallocated, releasing the associated view at the same time. But since view controller's views can be costly to create, it sometimes make sense to keep an additional external strong reference to a view controller inserted into a container. This way, when the view controller is removed from the container, it does not get destroyed, and neither does its view. For this caching mechanism to work, a container implementation must therefore never release the view associated to a view controller when it gets removed. As a corollary, this means that a container is only allowed to release a view when it needs to save memory. This usually has to be made after a memory warning has been received. For containers managing a large number of view controllers, this can also happen when the container decides to limit the number of views loaded at the same time. The picture below shows some container managing a stack of view controllers (only one is visible, but all currently inserted view controllers have been made visible for the sake of the explanation). In this case, the container only keeps the three topmost view controllers loaded, the views of the other ones have been unloaded:
A remark about properties allowing to retrieve the identity of the container view controller: Besides the navigationController and tabBarController properties, UIViewController also exhibits a read-only parentViewController property. According to UIKit documentation, "parent view controllers are relevant in navigation, tab bar, and modal view controller hierarchies" (iOS 4 documentation, with slight changes in iOS 5). Since custom containers are also parent view controllers in some way, it would be tempting to have this method return custom containers as well. This can in fact be achieved using method swizzling, and it seems to work quite well at first: When the parentViewController property has been swizzled, the interfaceOrientation property suddenly returns the correct orientation (in fact the one of the container), and the navigation bar title is forwarded from the content view controller to its container view controller transparently. Seems quite nice.
But doing so, there is no way to control whether or not we want the title to be forwarded or not. We therefore sadly cannot swizzle parentViewController if we want this flexibility (and I definitely wanted it). This seems to reveal the fact that Apple's vision of view controller containers was restricted to view controller containers like UITabBarController or UINavigationController. For such containers, forwarding perfectly makes sense in all cases since they display their contents "full screen".
In general, though, a container can display several view controllers simultaneously, in which case property forwarding may not make sense. Even if a single view controller is displayed, a container might want to have its own title. This is why the UIViewController parentViewController property should IMHO never be swizzled to return custom containers, even if this would have made sense.
Designing and implementing a view controller container
Now that we have extensively discussed the many details which we must pay attention to when dealing with view controllers, let us start discussing how a view controller container can be implemented. It took me several attempts to finally get a satisfying design and properly behaving objects. I here merely concentrate on general guidelines and hints, have a look at the full implementations in
CoconutKit for more juicy details.
Useful tools
Before starting to write a container, you should consider adding a set of test view controllers to your toolbox. Here is mine for CoconutKit, and it proved to be extremely useful:
A view controller logging lifecycle and rotation events to the console
Two view controllers with stretchable / non-stretchable views
A semi-transparent view controller to test stacking up view controllers
View controllers supporting only portrait, respectively landscape orientations
A view controller whose view is costly to create. Such an object can easily be simulated by calling sleep in its viewDidLoad method
A view controller customizing the title and navigation items when displayed (and providing a way to change these properties when already displayed)
In general, I use a random color as view background color, assigned in the viewDidLoad method. This way, I am able to easily see when a view controller has been unloaded and reloaded.
Moreover, you definitely need a way to test the behavior of your container when memory warnings are received. The easiest way IMHO is to present a view controller modally in the simulator on top of the container you are writing and testing, and to fake a memory warning before dismissing it. This helps you find quickly when your container fails at releasing views, but it should not prevent you from checking the process in detail at least once by setting breakpoints or adding logging statements: I discovered a
nasty bug in one of my view controllers this way
The toolbox being ready, let us discuss the implementation.
Managing view controllers in a container
As I was iterating through container implementations, I finally started to identify behavior traits common to view controllers managed by containers. Those traits are all formal and informal requirements we discussed previously. The most important step was to isolate these traits into a single class which can be reused by several container implementations. I called this class HLSContainerContent and, as its name suggests, its only purpose is to help managing the contents of a container, i.e. its view controllers.
An instance of HLSContainerContent acts as a kind of smart pointer, taking ownership of a view controller when it is handed over to a container, and releasing ownership when the object is destroyed. Instead of having the container directly retain the view controllers it manages (which is the proper semantics to have, as we already discussed), the container retains HLSContainerContent instances, each one wrapping a single view controller it retains. The end result is therefore the same.
What is interesting with smart pointer objects is that they can set up a context which stays valid during the time ownership has been acquired. In our case, this context can store the container controller into which a view controller has been inserted. This allows us to prevent simultaneous insertion into several containers, as well as to implement methods returning the parent container of a view controller. For built-in UIKit containers, such methods have been added using categories on UIViewController, we should therefore do the same. Since we cannot add any instance variables to the UIViewController class for storing which custom container it may have been inserted to (and this is not the way I would implement it if I could), we must resort to some kind of global mapping between view controllers and container objects. This could be implemented using a global dictionary, since there are no multithreading issues to consider here (UIKit is inherently meant to be used only from the main thread). Another convenient solution to achieve the same result is to use associated objects (see runtime.h). This namely offers an easy way to virtually add instance variables to classes which we do not control the implementation of, but beware if your code must be thread-safe.
Managing content is the primary goal of the HLSContainerContent class, but not its only one, as we will see later.
Managing view controller's views
As said before, accessing the view property of a view controller lazily creates the view. It is therefore especially important never to access this property sloppily. To reduce the probability of making mistakes when implementing a container, I decided that view operations should only occur through HLSContainerContent. While I did not implement any mechanism to enforce it, it is easy to check that a container implementation never directly access some view controller's view property directly.
The interface of an HLSContainerContent object exhibits four methods used to manage the view associated with the view controller it wraps:
A method to add the view controller's view where the containers draws its content (container view). If the view does not exist yet (it might already exist for caching purposes, see below), it gets instantiated and added to the container view. Some containers might manage a large number of view controllers, in which case they must be able to unload hidden views to save memory. When some unloaded view is about to be later displayed, it again needs to be instantiated and added. This is why additional HLSContainerContent objects can be provided as an additional parameter. Those represent the contents of the container, so that the view can be precisely inserted into the current container view hierarchy
A method to remove the view from the container view it was added to. This method does not release the view so that it might be reused without having to build it again from scratch
A method to release the view when it is not needed anymore, or when memory conditions degrade
Finally, a method returning the view if it has been instantiated, nil otherwise. This prevents the view from being instantiated too early
We previously discussed view caching, and why a container should not release the view associated with a view controller when it gets removed. For HLSContainerContent, this translated into separate methods for removing the view and releasing it. Moreover, the HLSContainerContent dealloc method only removes the view but does not release it. This way, even after a view controller has been removed from a container, its view can readily be used again if the view controller was retained somewhere else for caching purposes.
Transition animations
With UIKit built-in view controller containers, you cannot choose which transition animation you want to use when displaying a view controller. This is only possible when a view controller is presented modally, but I must admit I am not really fond of the way modal transitions are implemented in UIKit: A modalTransitionStyle property on UIViewController? Seriously, why not having an additional parameter to presentModalViewController:animated:? For my custom container interfaces, I thus decided to set transition styles using a parameter given to the various methods used for presenting view controllers. This is not only clearer from a user point of view, but also far cleaner in terms of object dependencies: The modalTransitionStyle UIViewController has nothing to do in UIViewController, since UIViewController objects should not be concerned about which objects display them. Imagine all nasty cyclic references this could lead to if all containers decided to do the same...
From a usability point of view, when a view controller is added with some animation, it must be removed with the exact same inverse animation so that the user does not get confused. It therefore makes sense to use HLSContainerContent to store the transition style with which a view controller has been presented. Containers which work as a stack can later use this information when the view controller is popped.
A question naturally arises: Since the animation style is stored in HLSContainerContent, why not implement the animation code in HLSContainerContent as well so that it can be shared by all custom containers? This is what I decided to do, and the resulting code is both shorter and easier to maintain, providing all my custom containers with a variety of animations, from the usual UIKit animations (cross-dissolve, navigation controller animations) to more exotic ones. And if I later think about a new animation, all I have to do is update a single class and all my containers can readily use it. Pretty cool, uh?
When implementing a container, being able to easily generate animations and corresponding reverse animations is especially important. Imagine a container managing a stack of view controllers. When you pop a view controller, you cannot be sure that the views and frames you have to animate are the same ones you animated when the view controller was pushed. Rotations might namely have occurred in between, or maybe the views were unloaded to save some memory (e.g. as a result of a memory warning). For this reason, I recommend building a transition animation right when you need it, based on the current context (the context being which transition style to use, whether we must play the reverse animation or not, and which views are animated). This way, you guarantee that your animation looks just right. As a corollary, be especially careful if you want to cache animation parameters. Consider whether it is justified from a performance point of view (hint: Probably not), and if it really is do not forget to invalidate your cache when a rotation or a memory warning occur.
In my case, I decided to implement a single HLSContainerContent public method through which I can get a transition animation for a view controller when I need it. To get the correct animation, this method expects some context data, like the current container contents and where views are drawn. This method then simply returns an HLSAnimation object, which encapsulates various animation details: Which views are animated, how they are, whether interaction is disabled during animation, etc. The HLSAnimation class also provides a way to generate the inverse animation with a single method call, even if the animation is complex, made of several steps and involving several views.
Lifecycle events
When implementing containers, the following two measures must be taken so that view lifecycle events reach contained view controllers correctly:
When a transition animation occurs, call the viewWillDisappear: and viewWillAppear: methods before the animation begins, for the view controllers which gets removed, respectively for the one which gets displayed. The animation start callback is the perfect place to insert this code. Conversely, call the viewDidDisappear: and viewDidAppear: methods in the animation end callback.
If the container is itself a view controller, it must forward the lifecycle events it receives to the currently active view controllers. This usually means sending the same events to the currently visible view controllers. For example, for a container stacking up view controllers, this would be the top one, and for a split view controller both view controllers involved. Getting this behavior is not difficult: Implement each view lifecycle method of the container view controller, call the super method, get the currently active view controllers, and call the same lifecycle method for each of them
Let us now discuss rotation.
Rotation events
If your container is a view controller, implement the usual one-step animation callbacks for it. This means:
The shouldAutorotateToInterfaceOrientation: should call the same method on all view controllers it contains. If a single one of them does not support the new orientation, return NO for the container as well.
In all subsequent one-step rotation events, call the super method, get the currently active view controllers and call the same rotation method for each of them
If your container is not a view controller (a case I admit I never had to implement until now), I conjecture your container should register for the UIApplicationWillChangeStatusBarOrientationNotification and UIApplicationDidChangeStatusBarOrientationNotification, and call the rotation methods for its contained view controllers from the associated callbacks. If you already had to implement such a container, I would be pleased to hear about your experience. I will try to update this article once I know more about this case.
In my view controller container implementations, I try to allow view controllers to be different depending on the interface orientation. In fact I defined a protocol, HLSOrientationCloner, which view controllers can implement to return a clone of themselves with a different orientation. In such cases, I use the willRotateToInterfaceOrientation:duration: method to instantiate the clone, and to start installing it with a cross-dissolve animation having the same duration as the rotation animation. If frame adjustments have to be done, those must not be made prior to willAnimateRotationToInterfaceOrientation:duration: being called.
One last piece of advice about rotations if your container is a view controller: I strongly recommend disabling rotation when a transition animation is playing. This could lead to undesired side-effects and is difficult to test. This should not be a nuisance to end-users who just need to try rotating their device again when the transition animation is over. An easy way to implement this behavior is to maintain an animation counter (incremented in an animation start callback, and decremented in the animation end callback), and to test it in the shouldAutorotateToInterfaceOrientation: method. If a running animation is discovered, just return NO to prevent rotation.
Pre-loading view controllers into a container
The user should always be able to pre-load a container with view controllers before it is actually displayed. Using HLSContainerContent, this was in fact easy to achieve. Pre-loading namely does not inadvertently lead to view creation, and all properties about the view controller and the associated transition animation are encapsulated as well. All that remains to do is to implement the viewWillAppear: container method (frame dimensions are not reliable earlier) to add the pre-loaded view controller's views to the container view.
Handling a large number of view controllers
Some containers might be able to load a large number of view controllers. Most of the time, such containers should fall into one of the following categories:
Containers with push-pop behavior: For such containers, it might make sense to only keep a limited number of topmost views alive, and to release those deeper (probably invisible anyway)
Containers which allow to browse a large number of view controllers, all of which should be accessible right from the beginning: In such cases, view controllers should be loaded lazily and released when appropriate, to avoid having to load them all right from the start (this could be costly even if no view is instantiated). How the release strategy should be implemented greatly depends on the container behavior. For example, if a container allows to browse view controllers in a contiguous manner, view controller's views far enough should be released
The code for handling large number of view controllers is inherently part of your container implementation, though again HLSContainerContent is designed to make view unloading and reloading easier. In most cases, a data source protocol can help formalizing the way view controllers are loaded.
Property forwarding
Last but not the least, HLSContainerContent also provides view controllers inserted into container view controllers with the ability to "see through" the container they are in, directly reaching any navigation controller the container is itself in. To achieve this result, the following properties have to be forwarded transparently through the container when this behavior is desired:
viewController.title
viewController.navigationItem.title
viewController.navigationItem.backBarButtonItem
viewController.navigationItem.titleView
viewController.navigationItem.prompt
viewController.navigationItem.hidesBackButton
viewController.navigationItem.leftBarButtonItem
viewController.navigationItem.rightBarButtonItem
viewController.toolbarItems
viewController.hidesBottomBarWhenPushed
As discussed previously, this cannot be simply achieved by having parentViewController return the container view controller. In the case of HLSContainerContainer, this was implemented by swizzling the following UIViewController getters and setters (thanks
0xced for the original idea and implementation):
navigationController
navigationItem
setTitle:
setHidesBottomBarWhenPushed:
setToolbarItems:
setToolbarItems:animated:
When one of the above getters is called and forwarding is enabled, we check if the view controller has been associated with a container view controller. If this is the case, the getter is recursively called on the container view controller, otherwise the original implementation is called. Having the getter recursively called ensures that forwarding works even if containers are nested.
Similarly, when one of the above setters is called and forwarding is enabled, the original implementation is called, and if the view controller is associated with a container view controller, the setter is also called on it, propagating the change further away.
Since we cannot simply swizzle the parentViewController method, we also have to fix the interfaceOrientation UIViewController read-only property. This is easily achieved by swizzling it, so that when a view controller is associated with a container view controller, the container orientation is returned.
The above approach works but is not perfect from a maintenance point of view, though: If more properties are added in the future, more methods will need to be swizzled. Any suggestion is welcome.
Examples of view controller containers
Now that we have discussed containers extensively, here are some examples of containers you might want to implement:
A container view controller embedding a single view controller (this is the practical example I described at the beginning of this article). My implementation is called HLSPlaceholderViewController. This container can be used to easily implement some kind of tab bar controller, for example
A container view controller managing a stack of view controllers. I implemented one, called HLSStackController, and which takes care of only keeping only a few top view controller's views loaded (this is a setting which can be tweaked if needed). Unlike what happens when view controllers are displayed modally, it can be used to show view controllers transparently on top of other ones
A container view controller displaying several view controllers side-by-side in a scroll view, one being visible at a time (maybe with paging enabled or periodic boundaries)
A container view controller displaying two view controllers side-by-side (similar to UISplitViewController)
I started with HLSPlaceholderViewController and HLSStackController since these containers correspond to basic building blocks which can be nested to create more complex containers. For example, by embedding an HLSStackController into an HLSPlaceholderViewController, you could create a new kind of navigation controller. By properly designing more high-quality containers and ensuring they nest properly, you can add precious tools to your iOS programmer toolbox. Start now!
One final remark about naming conventions I apply: I tend to name containers as HLS<SomeName>ViewController if they ultimately inherit from UIViewController and if they are intended to be subclassed when used. In all other cases, I simply call containers HLS<SomeName>Controller. This rule more or less fits the naming scheme applied for UIKit built-in view controllers, with some exceptions though (UISplitViewController, for example).
iOS 5
With the iOS 5 SDK, Apple engineers have introduced new API methods to help writing view controller containers. The documentation is currently rather scarce, but it seems the API can be quite useful to implement basic containers. It hasn't all the features of HLSContainerContent, but it is still a nice addition. It namely relieves the programmer from having to call most view lifecycle events (this is done for you), has support for transition animations, and introduces a clear parent-child relationship between a container and the view controllers it displays.
There is a major issue with this new API, though: Existing pre-iOS 5 container implementations need to be updated to run on iOS 5, otherwise silly bugs will appear (most notably doubled lifecycle events). This means:
The new UIViewController automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers method must be implemented to return NO
You must declare and manage containment relationships using the new addChildViewController: and removeFromParentViewController methods of UIViewController
Conclusion
This article has finally come to an end. It seems I have definitely pushed verbose mode too far this time, congratulations to those readers who could read this article in its entirety! I hope you now have a better understanding of view controllers, how they behave, and how to write containers which behave properly. There is so much to be said about view controllers, and I still have so much to discover about them, this article might only be the first in a long series. As always, comments and suggestions are welcome, and if you find errors please pinpoint them so I get fix them appropriately. Thanks in advance!
Sample code
The HLSContentContainer, HLSPlaceholderViewController, HLSStackController and HLSAnimation classes are available from the
CoconutKit framework. Their behavior can be tested by building and running the CoconutKit-dev project.