View Controller containment is a set of UIViewController
APIs that were introduced with iOS 5. I didn’t know much about it, or even really what the purpose of such a thing was. I’ve written a few container view controllers, mostly custom tab bar controllers, by using what I would call the “old and busted” way:
[selectedViewController viewWillDisappear:NO];
selectedViewController.view.hidden = YES;
[selectedViewController viewDidDisappear:NO];
vc = viewControllers[index];
selectedViewController = vc;
vc.view.frame = self.view.bounds;
[self.view addSubview:vc.view];
[vc viewWillAppear:NO];
vc.view.hidden = NO;
[vc viewDidAppear:NO];
This works, but it’s unreliable. I’m manually calling the appearance methods, but will rotation methods be called on every sub-viewcontroller? Memory warnings? What is the value of parentViewController
?
What do we get?
Forwarded methods
It turns out, using container view controllers cleans all that up for you, at a small upfront cost. The main purpose of it is to forward methods:
- Appearance methods:
-viewWillAppear
and friends - Rotation methods:
-willRotateToInterfaceOrientation
, etc - The memory warning method:
-didReceiveMemoryWarning
These methods are forwarded by default, and can be modified in iOS 5 and above with:
- (BOOL) automaticallyForwardAppearanceAndRotationMethodsToChildViewControllers
and iOS 6 and above with:
- (BOOL) shouldAutomaticallyForwardRotationMethods
- (BOOL) shouldAutomaticallyForwardAppearanceMethods
Properties
View controller containment also helps set properties like navigationController
, parentViewController
, and interfaceOrientation
correctly.
New stuff
We also get a few things we couldn’t really get before, also for free:
– isMovingFromParentViewController;
– isMovingToParentViewController;
- isBeingPresented;
– isBeingDismissed;
These faux-properties can be called during transitions, and give your app a better sense of its state.
So what is view controller containment?
View controller containment is simply a way of telling your parent and child view controllers which is which. They give us some methods, which are unfortunately very unclear and don’t tell us which are required and which are not.
- addChildViewController:
- removeFromParentViewController
- willMoveToParentViewController:
- didMoveToParentViewController:
-transitionFromViewController:toViewController:duration:options:animations:completion:
So, to explore containment, I wrote two small replacements for the UIKit
view controller containers, SKTabBarController
, and SKNavigationController
.
Both examples are in ARC.
SKTabBarController
SKTabBarController can be found at github.com/khanlou/SKTabBarController.
The tab bar controller is very simple.
- It has a tab bar, and it registers as the delegate of that tab bar.
- The
viewControllers
property can be set, and that adds all of the view controllers as child view controllers. - When a tab bar button is tapped, the delegate method is called, and
- If the already-selected button is being tapped, it pops to the root of that navigation controller.
- If a different button is being pressed, it removes the old view, sets the frame, and adds the new view.
Child View Controllers
The addChildViewController:
and removeFromParentViewController
can be thought of as view controller counterparts to addSubview:
and removeFromSuperview
. They’re asymmetric, meaning that one is called on the parent (“add”) and one is called on the child (“remove”). The view controller methods add view controllers to the childViewControllers
property, in the same way that the view methods add to the subviews
property.
The view controller methods MUST be called before the view methods.
Once a child is a member of parent view controller, simply calling addSubview:
or removeFromSuperview
will automatically call the appropriate appearance methods.
Child Callbacks
When calling addChildViewController:
and removeFromParentViewController
, you must let the view controller know that you are about to move it, and that’s where these methods come in:
- willMoveToParentViewController:
- didMoveToParentViewController:
When adding a child view controller,willMoveToParentViewController:
is automatically called, butdidMoveToParentViewController:
is not, and you must manually call it.
The reverse is the case for removing a child view controller. willMoveToParentViewController:
should be passed nil
, but didMoveToParentViewController:
will be called for you.
Proper Use Of childViewControllers
Originally, I had this container class adding children only when the button was tapped and removing them when a different button was tapped, but that feels wrong. A child is still a child, even if it’s not visible. You can browse the incorrect code at commit 297c2dc7a2. The current code is updated, and adds child view controllers when the viewControllers
property is set.
SKNavigationController
SKTabBarController can be found at github.com/khanlou/SKNavigationController.
I didn’t use any animations in SKTabBarController
, so things got more interesting when I tried to make a navigation controller.
The navigation controller still pretty straightforward.
- It has a navigation bar, and it registers as the delegate of that navigation bar.
- The
viewControllers
property can be set, and that adds all of the view controllers as child view controllers, and pushes their navigation items onto the navigation bar. - The push and pop methods of the navigation controller use
-transitionFromViewController:toViewController:duration:options:animations:completion:
for animations.
Pushing a New View Controller
When pushing a new view controller, we must do several things.
- Add it to the parent view controller.
- Create a navigation item for it, and add that item to the navigation bar.
- If animated, we need to set up a transition.
- At the end of the animation, we need to tell the pushed view controller that it has been fully moved to the parent.
Transitions are weird. They’re like traditional UIView
animations, but they do more things for you.
[self transitionFromViewController:oldViewController
toViewController:newViewController
duration:0.35f
options:0
animations:^{
newViewController.view.center = oldViewController.view.center;
oldViewController.view.center = CGPointMake(oldViewController.view.center.x - self.view.bounds.size.width, oldViewController.view.center.y);
}
completion:^(BOOL finished) {
[newViewController didMoveToParentViewController:self];
}
];
Let’s go through it step-by-step.
fromViewController
andtoViewController
must exist, and have the same parent.options
can include anyUIView
animation options, including several of the built in animation transitions.animations
can be used for custom animations.completion
is used to call anydidMoveToParentViewController:
callback methods, and cleanup for your animation.
Adding and Removing the Subview
transitionFromViewController:
automatically calls addSubview:
with toViewController.view
. Calling it yourself will result in the incredibly frustrating >
Unbalanced calls to begin/end appearance transitions for
It will also automatically call removeFromSuperview
on fromViewController.view
.
Popping an View Controller off the Stack
Popping is very similar to pushing.
- Tell the view controller coming off the stack that it is being moved to a
nil
parent. - Pop the navigation item off the navigation bar.
- Set up the transition.
- Upon completion, fully remove the popped navigation controller from the parent.
I won’t include the code for the sake of brevity.
Takeaways
There are three takeaways:
- Adding a child view controller must be followed by
didMoveToParentViewController:self
. - Removing a child view controller must be preceded by
willMoveToParentViewController:nil
. - Transitions automatically handle adding and removing of the child view controllers’ views.