48 Comments

Switching Child View Controllers in iOS with Auto Layout

In this post, I will show you how to switch between two child view controllers that are constrained with auto layout.

I struggled with the name of this post since I called my last post in this series The Easy Way to Switch Container Views in iOS. I didn’t want to say this method is “the hard way” or “the right way” because the “easy” method might be the right method if the downsides I mentioned in my last post are not an issue for you. The next title I considered was “Switching Container Views with Auto Layout,” but that’s not technically correct since it is not possible to have a single container view with two child view controllers. If you want this result, it has to be done manually without container views and embedded segues. Now that I have settled on a title, on to the solution.

I am going to modify the example that I used in my previous post. Previously, I toggled the visibility of two container views identified as A and B, using a segmented control to switch between them. In this example, I will still use the segmented control, but the way the view controller is constructed is totally different. If you want to follow along, I put the entire project on GitHub

Switching Component Views

Building the Example

1. First, add a plain UIView or container view to your view controller. Now delete the embedded segue that connects the container view to the child view controller. A container view without a child is simply a UIView, so it doesn’t matter which one you use. I like using the container view because on the storyboard, it has a watermark that indicates this view is a container of other view controllers.

2. If you use a UIView, drag a view controller next to the parent that will become the child. If you use a container view, you should already have a child view controller on the storyboard. I applied a blue color to the child view controller and called it “Component A” so you could tell the difference between the two.

3. Next, position and constrain the container view however you want it to appear in your parent view controller.

4. Drag a second child view controller underneath Component A. I applied a purple color to the child view controller and called it “Component B”.

At this point, your layout should look like this:
finished_layout_of_child_view_controllers

One of the advantages of using container views and embedded segues is that any layout change you make to your container view will be applied to the child view controller in interface builder. That is not the case with the two child view controllers since they are not connected with an embedded segue. They appear as a full-size view controller. However, we can apply a freeform size to our child view controllers to approximate their finished size. Adding a freeform size in interface builder does not affect the size of the view controller when you run the app.

5. Add a freeform size to both child view controllers to match the size of your container view.

freeform_size

6. Because we will be instantiating the child view controllers on demand, we need to give them storyboard identifiers before we create them in code. Add a storyboard identifier to each child view controller.

add_identifiers_to_children

Adding the First Child

Our visual layout is now done. If we build and run our application right now, neither child view will be shown because we don’t have the container view’s embedded segue that automatically adds the first child view controller to the container view. We can fix this by adding the child view controller to the container view manually with our viewDidLoad method.


class ViewController: UIViewController {
    @IBOutlet weak var containerView: UIView!
    weak var currentViewController: UIViewController?
    
    override func viewDidLoad() {
        self.currentViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ComponentA")
        self.currentViewController!.view.translatesAutoresizingMaskIntoConstraints = false
        self.addChildViewController(self.currentViewController!)
        self.addSubview(self.currentViewController!.view, toView: self.containerView)
        super.viewDidLoad()
    }

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (weak, nonatomic) UIViewController *currentViewController;

@end

@implementation ViewController

- (void)viewDidLoad {
    self.currentViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ComponentA"];
    self.currentViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
    [self addChildViewController:self.currentViewController];
    [self addSubview:self.currentViewController.view toView:self.containerView];
    [super viewDidLoad];
}

The first line of code instantiates the child view controller named “Component A”. Next, we set the auto resizing mask appropriately on the view of the newly constructed child view controller for use with auto layout. Then we add the child view controller to the parent view controller.

The final line of code in the viewDidLoad method uses a helper method I wrote to add a sub view to another view and constrain it with auto layout. The constraints bind the subview directly to the edges of the parent view. We will make use of this method later when we want to switch between child view controllers.


func addSubview(subView:UIView, toView parentView:UIView) {
    parentView.addSubview(subView)

    var viewBindingsDict = [String: AnyObject]()
    viewBindingsDict["subView"] = subView
    parentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[subView]|",
        options: [], metrics: nil, views: viewBindingsDict))
    parentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[subView]|",
        options: [], metrics: nil, views: viewBindingsDict))
}

- (void)addSubview:(UIView *)subView toView:(UIView*)parentView {
    [parentView addSubview:subView];
    
    NSDictionary * views = @{@"subView" : subView,};
    NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[subView]|"
                                                                   options:0
                                                                   metrics:0
                                                                     views:views];
    [parentView addConstraints:constraints];
    constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[subView]|"
                                                          options:0
                                                          metrics:0
                                                            views:views];
    [parentView addConstraints:constraints];
}

If you build and run with what you have now, you will see that Component A is shown initially. Now, let’s start thinking about how to switch over to Component B.

Using the Segmented Control

Just like my previous blog post, I am using a segmented control to switch from A to B. I created an outlet on the segmented control for the “value changed” action called showComponent. When the selectedSegmentIndex is zero, I need to show A. Otherwise, I need to show B.

I chose to instantiate my two child view controllers dynamically every time I switch from A to B. If you only have a couple of child view controllers, this step is unnecessary. Instead, you would construct them once and keep a reference to them. However, if you have many child view controllers or ones that consume a lot of memory, then constructing them as needed is the way to go.

The code is pretty straightforward. I construct the view controllers in the same way I did in the viewDidLoad method and I call a helper method called cycleFromViewController:toViewController: to do the transition. We will look at how this helper method works later in this blog post.


@IBAction func showComponent(sender: UISegmentedControl) {
    if sender.selectedSegmentIndex == 0 {
        let newViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ComponentA")
        newViewController!.view.translatesAutoresizingMaskIntoConstraints = false
        self.cycleFromViewController(self.currentViewController!, toViewController: newViewController!)
        self.currentViewController = newViewController
    } else {
        let newViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ComponentB")
        newViewController!.view.translatesAutoresizingMaskIntoConstraints = false
        self.cycleFromViewController(self.currentViewController!, toViewController: newViewController!)
        self.currentViewController = newViewController
    }
}

- (IBAction)showComponent:(UISegmentedControl *)sender {
    if (sender.selectedSegmentIndex == 0) {
        UIViewController *newViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ComponentA"];
        newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
        [self cycleFromViewController:self.currentViewController toViewController:newViewController];
        self.currentViewController = newViewController;
    } else {
        UIViewController *newViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"ComponentB"];
        newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;
        [self cycleFromViewController:self.currentViewController toViewController:newViewController];
        self.currentViewController = newViewController;
    }
}

Transitioning between View Controllers without Auto Layout

Before I get too far into demonstrating a manual transition between two child view controllers, let’s look at some sample code from Apple on how to transition between two view controllers by adjusting the frame of the views instead of using auto layout.


- (void)cycleFromViewController: (UIViewController*) oldVC
               toViewController: (UIViewController*) newVC {
   // Prepare the two view controllers for the change.
   [oldVC willMoveToParentViewController:nil];
   [self addChildViewController:newVC];
 
   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.
   newVC.view.frame = [self newViewStartFrame];
   CGRect endFrame = [self oldViewEndFrame];
 
   // Queue up the transition animation.
   [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
            // Animate the views to their final positions.
            newVC.view.frame = oldVC.view.frame;
            oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
           // Remove the old view controller and send the final
           // notification to the new view controller.
           [oldVC removeFromParentViewController];
           [newVC didMoveToParentViewController:self];
        }];
}

When you try to convert this method to use auto layout instead of setting the frames, you run into a problem. You can’t add auto layout constraints between two views unless they are in the same view hierarchy. The transitionFromViewController:toViewController method does not give us a callback between attaching the view to the hierarchy and calling the animation block. We can get around this by doing what the transitionFromViewController:toViewController method does manually.

Switching between View Controllers with Auto Layout

We can avoid using the transitionFromViewController:toViewController method by using the custom container view controller api to do the transition of the view controllers ourselves. I also used the UIView animation api to animate the transition. This is my version of the cycleFromViewController:toViewController method.


func cycleFromViewController(oldViewController: UIViewController, toViewController newViewController: UIViewController) {
    oldViewController.willMoveToParentViewController(nil)
    self.addChildViewController(newViewController)
    self.addSubview(newViewController.view, toView:self.containerView!)
    newViewController.view.alpha = 0
    newViewController.view.layoutIfNeeded()
    UIView.animateWithDuration(0.5, animations: {
            newViewController.view.alpha = 1
            oldViewController.view.alpha = 0
        },
        completion: { finished in
            oldViewController.view.removeFromSuperview()
            oldViewController.removeFromParentViewController()
            newViewController.didMoveToParentViewController(self)
    })
}

- (void)cycleFromViewController:(UIViewController*) oldViewController
               toViewController:(UIViewController*) newViewController {
    [oldViewController willMoveToParentViewController:nil];
    [self addChildViewController:newViewController];
    [self addSubview:newViewController.view toView:self.containerView];
    [newViewController.view layoutIfNeeded];

    // set starting state of the transition
    newViewController.view.alpha = 0;
    
    [UIView animateWithDuration:0.5
                     animations:^{
                         newViewController.view.alpha = 1;
                         oldViewController.view.alpha = 0;
                     }
                     completion:^(BOOL finished) {
                         [oldViewController.view removeFromSuperview];
                         [oldViewController removeFromParentViewController];
                         [newViewController didMoveToParentViewController:self];
                     }];
}

Switching Component Views

I matched the same fade transition I did in my previous blog post, but you could do better. Since we are using auto layout, you can do any transition you need by modifying the constraints. Here is the same function again with placeholders showing where to set up your constraints to animate the transition. Inside the animation block, you only need to call layoutIfNeeded.


func cycleFromViewController(oldViewController: UIViewController, toViewController newViewController: UIViewController) {
    oldViewController.willMoveToParentViewController(nil)
    self.addChildViewController(newViewController)
    self.addSubview(newViewController.view, toView:self.containerView!)
    // TODO: Set the starting state of your constraints here
    newViewController.view.layoutIfNeeded()

    // TODO: Set the ending state of your constraints here

    UIView.animateWithDuration(0.5, animations: {
            // only need to call layoutIfNeeded here
            newViewController.view.layoutIfNeeded()
        },
        completion: { finished in
            oldViewController.view.removeFromSuperview()
            oldViewController.removeFromParentViewController()
            newViewController.didMoveToParentViewController(self)
    })
}

- (void)cycleFromViewController:(UIViewController*) oldViewController
               toViewController:(UIViewController*) newViewController {
    [oldViewController willMoveToParentViewController:nil];
    [self addChildViewController:newViewController];
    [self addSubview:newViewController.view toView:self.containerView];
    // TODO: Set the starting state of your constraints here
    [newViewController.view layoutIfNeeded];

    // TODO: Set the ending state of your constraints here
    
    [UIView animateWithDuration:0.5
                     animations:^{
                         // only need to call layoutIfNeeded here
                         [newViewController.view layoutIfNeeded];
                     }
                     completion:^(BOOL finished) {
                         [oldViewController.view removeFromSuperview];
                         [oldViewController removeFromParentViewController];
                         [newViewController didMoveToParentViewController:self];
                     }];
}

Just the results we wanted! If you would like to try this code out yourself to see it in action, I have the entire project on GitHub.


Catch up on my other posts in this series:


For more on using Xcode, read my series on unwind segues: