Container View Controllers

The basic building block for most iOS apps is the view controller. Regardless of whether the app's design calls for the display of a map, or a table, or a means to gather user details, the view controller provides an elegant manner of interacting with the user.

Most developers realize that a view controller encapsulates a view, and that view can contain several "sub views" (e.g. buttons, labels, and text fields). However, starting in iOS 5, Apple allows a view controller to contain another view controller. (When a view controller contains "child" controllers, it is said to be a container view controller.) This article will provide some background on container view controllers, and show two examples of how to use them.

Container view controllers are very powerful, and allow a developer to aggregate a series of smaller view controllers into a richer experience for the user. Unfortunately, there seems to be a lack of material that clearly explains how to use container view controllers. Most examples are either overly complex, or incomplete. (I even saw an example that didn't use an actual view controller.) I'm not claiming to have the ideal example, but my hope is to provide a simple, end-to-end example which shows how to add, remove, and transition contained view controllers.

The MVC Design Pattern

When writing software, most developers follow design patterns. (From wikipedia: A design pattern is a general reusable solution to a commonly occurring problem within a given context in software design.) When it comes to programs that have a user interface, it's very popular for developers to use the model-view-controller (MVC) design pattern.

With iOS, Apple strongly encourages a specific type of MVC - one in which a controller concerns itself mostly with the view layer. Accordingly, views are wrapped in something called a "view controller". (For example: UIViewController.) Most of the time, this approach doesn't add unnecessary complexity. Unfortunately, when it comes to container view controllers, things get complex quickly.

Without getting into all of the details, there is both a view controller hierarchy, and a "view only" hierarchy. In some situations, iOS handles the dual relationships for the developer. In other places, it does not. The result is an unnecessarily complex pair of relationships that cause "bad things" to happen if the relationships become mixed up. Care must therefore be taken to ensure consistency between the dual hierarchies.

As an aside, Android does not force a view controller design pattern. However, for the sake of compatibility and simplicity, it's generally a good idea to use the same design approach on both platforms.

With that in place, it's time to dive into two examples.

Example 1: A very simple example

The purpose of this example is to demonstrate how to add and remove a view controller (and it's view) from a container view controller. The app consists of three view controllers:

  • ContainerViewController - The name says it all... it's the view controller that will contain the content view controllers
  • ButtonViewController - A simple view controller that contains two buttons.
  • OrangeViewController - A simple view controller that contains two text labels. It also has an orange background to make it easy to see the bounds of the view.

The behavior of the app is minimal: When the Show / Hide button is clicked, the orange view will be either removed or added as a child view controller and view.

Note: ContainerViewController does *not* have an associated xib file, and it will be instantiated by using the default init: constructor.

The two remaining xib files are as follows:


There is no (meaningful) source code associated with OrangeViewController.

ButtonViewController is defined as follows:

  1. #import <UIKit/UIKit.h>
  2. #import "ContainerViewController.h"
  3.  
  4. @interface ButtonViewController : UIViewController
  5.  
  6. -(void)setController: (ContainerViewController *)vc;
  7. -(IBAction)buttonClicked:(id)sender;
  8.  
  9. @end
  1. #import "ButtonViewController.h"
  2.  
  3. @implementation ButtonViewController {
  4. ContainerViewController *_vc;
  5. }
  6.  
  7. -(void)setController: (ContainerViewController *)vc {
  8. _vc = vc;
  9. }
  10.  
  11. -(IBAction)buttonClicked:(id)sender {
  12. [_vc toggleTextViewVisible];
  13. }
  14.  
  15. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
  16. {
  17. return [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  18. }
  19. @end

In the above code, I've taken a shortcut in the name of simplicity: I need a way to notify the container view controller when a button is clicked. Normally, I should define a protocol for this. However, to keep things concise, I'm simply passing a reference to the container view controller. (Don't take this shortcut when you're writing "real" code.)

Besides the shortcut, this code is straightforward: When the show/hide button is clicked, toggleTextViewVisible: is invoked.

The files AppDelegate.h and AppDelegate.m are as follows:

  1. #import <UIKit/UIKit.h>
  2.  
  3. @interface AppDelegate : UIResponder <UIApplicationDelegate>
  4. @property (strong, nonatomic) UIWindow *window;
  5.  
  6. @end
  1. #import "AppDelegate.h"
  2.  
  3. #import "ContainerViewController.h"
  4.  
  5. @implementation AppDelegate
  6.  
  7. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
  8. {
  9. self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
  10.  
  11. // Don't use a NIB/XIB for this view contoller
  12. ContainerViewController *containerVC = [[ContainerViewController alloc] init];
  13. self.window.rootViewController = containerVC;
  14.  
  15. [self.window makeKeyAndVisible];
  16. return YES;
  17. }
  18.  
  19. @end

There's not much magic here: The app instantiates a container view controller, and makes it the rootViewController.

Now for the nontrivial stuff! The interface and implementation of the ContainerViewController are as follows:

ContainerViewController.h:

  1. #import <UIKit/UIKit.h>
  2.  
  3. @interface ContainerViewController : UIViewController
  4.  
  5. // (Ideally, this would be in a protocol instead)
  6. -(void) toggleTextViewVisible;
  7.  
  8. @end

ContainerViewController.m:

  1. #import "ContainerViewController.h"
  2. #import "ButtonViewController.h"
  3. #import "OrangeViewController.h"
  4.  
  5. @implementation ContainerViewController {
  6. ButtonViewController *buttonVC;
  7. OrangeViewController *orangeVC;
  8. BOOL bottomIsVisible;
  9. }
  10.  
  11. - (void)viewDidLoad
  12. {
  13. [super viewDidLoad];
  14.  
  15. // Instantiate two contained views
  16. buttonVC = [[ButtonViewController alloc] initWithNibName:@"ButtonViewController" bundle:nil];
  17. [self addChildViewController:buttonVC];
  18. [self.view addSubview:buttonVC.view];
  19. [buttonVC didMoveToParentViewController:self];
  20.  
  21. orangeVC = [[OrangeViewController alloc] initWithNibName:@"OrangeViewController" bundle:nil];
  22. [self addChildViewController:orangeVC];
  23. [self.view addSubview:orangeVC.view];
  24. [orangeVC didMoveToParentViewController:self];
  25.  
  26. // The button view controller needs to notify the parent container when the buttons are clicked
  27. [buttonVC setController:self];
  28.  
  29. bottomIsVisible = YES;
  30. }
  31.  
  32. -(void)viewWillAppear:(BOOL)animated {
  33. [super viewWillAppear:animated];
  34. [self updateViewSizes];
  35. }
  36.  
  37. -(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
  38. [self updateViewSizes];
  39. }
  40.  
  41. // Recalculate the size of the two displayed views - even if the bottom view is not currently visible
  42. -(void) updateViewSizes {
  43. CGRect bounds1 = CGRectInset(self.view.bounds, 10, 10);
  44. CGRect bounds2 = CGRectInset(self.view.bounds, 10, 10);
  45. bounds1.size.height = bounds1.size.height / 2;
  46. bounds2.origin.y = bounds2.origin.y + bounds1.size.height + 4;
  47. bounds2.size.height = bounds1.size.height;
  48.  
  49. buttonVC.view.frame = bounds1;
  50. orangeVC.view.frame = bounds2;
  51. }
  52.  
  53. // The 'Show / Hide' button has been clicked
  54. -(void) toggleTextViewVisible {
  55. if (bottomIsVisible == YES) {
  56. // This demonstrates how to remove a view controller (and it's view) from a container view controller
  57. [orangeVC willMoveToParentViewController:nil];
  58. [orangeVC.view removeFromSuperview];
  59. [orangeVC removeFromParentViewController];
  60. bottomIsVisible = NO;
  61. } else {
  62. // This demonstrates how to add a view controller (and it's view) to a container view controller
  63. [self addChildViewController:orangeVC];
  64. [self.view addSubview:orangeVC.view];
  65. [orangeVC didMoveToParentViewController:self];
  66. bottomIsVisible = YES;
  67. }
  68. }
  69. @end

Comments regarding the implementation file:

  • Lines 6 - 8: Three local properties that are not visible outside of the class. They hold pointers to two view controllers, and a boolean to specify if the 2nd view is a child of the container.
  • Line 16-19: Instantiate the buttonVC property. Once initialized, add it as a child to the container view controller. Also, add the view associated with buttonVC as a child of the ContainerViewController's view. Lastly, inform buttonVC that it's partent view controller has changed.
  • Lines 21 - 24: Perform similar tasks for the second view controller (orangeVC).
  • Line 27: ButtonViewController has a public method for setting the container. This is not directly related to this example. Instead, it's a means of telling the container view controller that a button has been clicked.
  • Lines 32 - 39: When the view appears, or when it changes orientation, recalculate the child view sizes.
  • Lines 41 - 51: Calculate the size of the frames for each child view.
  • Lines 53 - 68: Depending on the current visibility of the bottom view, either add or remove orangeVC as a child view container and view.

That's it! And the most important parts of the code are where the view is either added or removed.

To add a child view controller and view:

  1. [self addChildViewController:orangeVC];
  2. [self.view addSubview:orangeVC.view];
  3. [orangeVC didMoveToParentViewController:self];

To remove a child view controller and view:

  1. [orangeVC willMoveToParentViewController:nil];
  2. [orangeVC.view removeFromSuperview];
  3. [orangeVC removeFromParentViewController];

When the application is launched, it looks as follows:

After clicking the Show / Hide button, the screen is updated as follows:

After clicking the button again, and rotating the device, the screen looks as follows:

Done and done. I'll now go on to a (slightly) more advanced example.

Example 2: Transitioning Between Child View Controllers

The prior example touched on two key operations: adding view controllers to a parent container, and removing them from a parent controller. However, there's actually a third operation that's even more powerful: a transition from displaying one child view controller to another. This example will build upon the prior example, and demonstrate how to perform a transition.

I'll now add code which responds to the clicking of the "Swap Views" button: If the bottom view is visible, it will swap between two different child view controllers.

To demonstrate this functionality, I'll add a third view controller: BlueViewController. It will be exactly the same as OrangeViewController, except (as you've no doubt guessed), the background will be blue:

As far as the interface and implementation files go, they're basically blank:

BlueViewController.h

  1. #import <UIKit/UIKit.h>
  2. @interface BlueViewController : UIViewController
  3. @end

BlueViewController.m

  1. #import "BlueViewController.h"
  2.  
  3. @implementation BlueViewController
  4.  
  5. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
  6. return [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  7. }
  8.  
  9. @end

Next, I'll modify ButtonViewController.m to invoke a method in ContainerViewController when the "Swap Views" method has been clicked:

  1. #import "ButtonViewController.h"
  2.  
  3. @implementation ButtonViewController {
  4. ContainerViewController *_vc;
  5. }
  6.  
  7. -(void)setController: (ContainerViewController *)vc {
  8. _vc = vc;
  9. }
  10.  
  11. -(IBAction)buttonClicked:(id)sender {
  12. [_vc toggleTextViewVisible];
  13. }
  14.  
  15. -(IBAction)button2Clicked:(id)sender {
  16. [_vc swapViews];
  17. }
  18.  
  19. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
  20. {
  21. return [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  22. }
  23.  
  24. @end

The only new code is the button2Clicked: method.

Turning to ContainerViewController, I've modified the interface to expose the swapViews: method:

  1. #import <UIKit/UIKit.h>
  2.  
  3. @interface ContainerViewController : UIViewController
  4.  
  5. // Two public methods invoked by the button view controller
  6. // (Ideally, they would be in a protocol instead)
  7. -(void) toggleTextViewVisible;
  8. -(void) swapViews;
  9.  
  10. @end

Lastly, I update ContainerViewController.m as follows:

  1. #import "ContainerViewController.h"
  2. #import "ButtonViewController.h"
  3. #import "OrangeViewController.h"
  4. #import "BlueViewController.h"
  5.  
  6. @implementation ContainerViewController {
  7. ButtonViewController *buttonVC;
  8. OrangeViewController *orangeVC;
  9. BlueViewController *blueVC;
  10. UIViewController *curBottomVC;
  11. BOOL bottomIsVisible;
  12. }
  13.  
  14. - (void)viewDidLoad
  15. {
  16. [super viewDidLoad];
  17.  
  18. // Instantiate three contained views, but only add two to this container (for now)
  19. buttonVC = [[ButtonViewController alloc] initWithNibName:@"ButtonViewController" bundle:nil];
  20. [self addChildViewController:buttonVC];
  21. [self.view addSubview:buttonVC.view];
  22. [buttonVC didMoveToParentViewController:self];
  23.  
  24. orangeVC = [[OrangeViewController alloc] initWithNibName:@"OrangeViewController" bundle:nil];
  25. [self addChildViewController:orangeVC];
  26. [self.view addSubview:orangeVC.view];
  27. [orangeVC didMoveToParentViewController:self];
  28.  
  29. blueVC = [[BlueViewController alloc] initWithNibName:@"BlueViewController" bundle:nil];
  30.  
  31.  
  32. // The button view controller needs to notify the parent container when the buttons are clicked
  33. [buttonVC setController:self];
  34.  
  35. // The currently displayed bottom view controller is the text view controller
  36. curBottomVC = orangeVC;
  37.  
  38. bottomIsVisible = YES;
  39. }
  40.  
  41. -(void)viewWillAppear:(BOOL)animated {
  42. [super viewWillAppear:animated];
  43. [self updateViewSizes];
  44. }
  45.  
  46. -(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
  47. [self updateViewSizes];
  48. }
  49.  
  50. // Recalculate the size of the two displayed views - even if the bottom view is not currently visible
  51. -(void) updateViewSizes {
  52. CGRect bounds1 = CGRectInset(self.view.bounds, 10, 10);
  53. CGRect bounds2 = CGRectInset(self.view.bounds, 10, 10);
  54. bounds1.size.height = bounds1.size.height / 2;
  55. bounds2.origin.y = bounds2.origin.y + bounds1.size.height + 4;
  56. bounds2.size.height = bounds1.size.height;
  57.  
  58. buttonVC.view.frame = bounds1;
  59. curBottomVC.view.frame = bounds2;
  60. }
  61.  
  62. // The 'Show / Hide' button has been clicked
  63. -(void) toggleTextViewVisible {
  64. if (bottomIsVisible == YES) {
  65. // This demonstrates how to remove a view controller (and it's view) from a container view controller
  66. [curBottomVC willMoveToParentViewController:nil];
  67. [curBottomVC.view removeFromSuperview];
  68. [curBottomVC removeFromParentViewController];
  69. bottomIsVisible = NO;
  70. } else {
  71. // This demonstrates how to add a view controller (and it's view) to a container view controller
  72. [self addChildViewController:curBottomVC];
  73. [self.view addSubview:curBottomVC.view];
  74. [curBottomVC didMoveToParentViewController:self];
  75. bottomIsVisible = YES;
  76. }
  77. }
  78.  
  79. // The 'Swap Views' button has been clicked
  80. -(void)swapViews {
  81. if (bottomIsVisible) {
  82. if (curBottomVC == orangeVC) {
  83. [self swapFrom:orangeVC to:blueVC ];
  84. curBottomVC = blueVC;
  85. } else {
  86. [self swapFrom:blueVC to:orangeVC ];
  87. curBottomVC = orangeVC;
  88. }
  89. }
  90. }
  91.  
  92. -(void) swapFrom: (UIViewController *) oldVC to: (UIViewController *) newVC {
  93. [oldVC willMoveToParentViewController:nil];
  94. [self addChildViewController:newVC]; // Calls willMoveToParentController: self
  95.  
  96. newVC.view.frame = oldVC.view.frame;
  97.  
  98. // This method AUTOMATICALLY handles adding and removing the views
  99. [self transitionFromViewController:oldVC toViewController:newVC duration:.5 options:UIViewAnimationOptionTransitionCurlDown
  100. animations:^{
  101. }
  102. completion:^(BOOL finished) {
  103. [oldVC removeFromParentViewController]; // Calls didMoveToParentViewController: nil
  104. [newVC didMoveToParentViewController:self];
  105. }
  106. ];
  107. }
  108.  
  109. @end

Comments:

  • Lines 9-10: I've added an instance of BlueViewController and a generic view controller named curBottomVC. This property will point to the currently active bottom view controller.
  • Line 29: Instantiate blueVC, but don't add it to the container view controller.
  • Line 36: Specify that the orange view controller is the currently active bottom view controller. (I'll toggle this setting later.)
  • Line 59: I've modified updateViewSizes: to reference curBottomVC instead of orangeVC.
  • Lines 62 - 77: As with the prior step, I now reference curBottomVC instead of orangeVC.
  • Lines 79 - 90: If the bottom view has been included (and is therefore visible), swap between the orange and blue view controllers.
  • Lines 92 - 107: This method handles the actual transition between one subview and another:
    • Notify the view controllers that things are about to change
    • Set the frame size of the new view to equal the old view.
    • Invoke transitionFromViewController:. This code handles the adding and removing of the views associated with the view controllers.
    • The transition will be a "fancy" curl down. There are other sorts of transitions available too.
    • When the transition is done, notify the view controllers that the changes are complete.

When the app is launched, it initially appears as so:

Clicking the "Swap Views" button transitions the view controllers as follows:

When the "Show Hide" button is clicked, and the bottom view is removed, the "Swap Views" button no longer works. (Ideally, I should disable the button, but that's an exercise left to the reader.)

And lastly, I can rotate the screen, re-add the child view, and see that the blue view is properly positioned.

At this point, I've presented a simple example which demonstrates how to add, remove, and swap child view controllers.

Summary

Container view controllers provide developers with powerful new functionality. Unfortunately, the available documentation makes this difficult to grasp. To me, there are a few key things to keep in mind when writing a container view controller:

1. The container view controller is responsible for setting the position and size of the child views. This should seem obvious, but it gets lost in the details.

2. Be sure to address both the view controller and the underlying view. The iOS API tries to help the developer, but in my opinion, it makes things more confusing. When in doubt, remember the following from the examples:

To add a child view controller:

  1. [self addChildViewController:curBottomVC];
  2. [self.view addSubview:curBottomVC.view];
  3. [curBottomVC didMoveToParentViewController:self];

To remove a child view controller:

  1. [curBottomVC willMoveToParentViewController:nil];
  2. [curBottomVC.view removeFromSuperview];
  3. [curBottomVC removeFromParentViewController];

To transition between child view controllers:

  1. [oldVC willMoveToParentViewController:nil];
  2. [self addChildViewController:newVC]; // Calls willMoveToParentController: self
  3.  
  4. newVC.view.frame = oldVC.view.frame;
  5.  
  6. // This method AUTOMATICALLY handles adding and removing the views
  7. [self transitionFromViewController:oldVC toViewController:newVC duration:.5 options:UIViewAnimationOptionTransitionCurlDown
  8. animations:^{
  9. }
  10. completion:^(BOOL finished) {
  11. [oldVC removeFromParentViewController]; // Calls didMoveToParentViewController: nil
  12. [newVC didMoveToParentViewController:self];
  13. }
  14. ];

As mentioned, the transition method handles the adding and removing of the child views, and if I attempt to do it manually, I'll get an error in my console similar to the following: Unbalanced calls to begin/end appearance transitions.

3. It's possible to mess up the view controller / view linkages, but keep things simple, and this won't be a problem.

With these tips in mind, I suspect you'll have no trouble writing an elegant container view controller. Best of luck! :-)