How to use Auto Layout with container transitions? How to use Auto Layout with container transitions? ios ios

How to use Auto Layout with container transitions?


Starting to think the utility methodtransitionFromViewController:toViewController:duration:options:animations:completion can not be made to work cleanly with Auto Layout.

For now I've replaced my use of this method with calls to each of the "lower level" containment methods directly. It is a bit more code but seems to give greater control.

It looks like this:

- (void) performTransitionFromViewController:(UIViewController*)fromVc toViewController:(UIViewController*)toVc {    [fromVc willMoveToParentViewController:nil];    [self addChildViewController:toVc];    UIView *toView = toVc.view;    UIView *fromView = fromVc.view;    [self.containerView addSubview:toView];    // TODO: set initial layout constraints here    [self.containerView layoutIfNeeded];    [UIView animateWithDuration:.25                          delay:0                        options:0                     animations:^{                         // TODO: set final layout constraints here                         [self.containerView layoutIfNeeded];                     } completion:^(BOOL finished) {                         [toVc didMoveToParentViewController:self];                         [fromView removeFromSuperview];                         [fromVc removeFromParentViewController];                     }];}


The real solution seems to be to set up your constraints in the animation block of transitionFromViewController:toViewController:duration:options:animations:.

[self transitionFromViewController:fromViewController                  toViewController:toViewController                          duration:1.0                           options:UIViewAnimationOptionTransitionCurlDown                        animations:^{                            // SET UP CONSTRAINTS HERE                        }                        completion:^(BOOL finished) {                            [toViewController didMoveToParentViewController:self];                            [fromViewController removeFromParentViewController];                        }];


There are two solutions depending on whether you simply need to position the view via auto layout (easy) vs. needing to animate auto layout constraint changes (harder).

TL;DR version

If you only need to position a view via auto layout, you can use the -[UIViewController transitionFromViewController:toViewController:duration:options:animations:completion:] method and install the constraints in the animation block.

If you need to animate auto layout constraint changes, you must use a generic +[UIView animateWithDuration:delay:options:animations:completion:] call and add the child controller regularly.

Solution 1: Position a view via Auto Layout

Let's tackle the first, easy case first. In this scenario, the view should be positioned via auto layout so that changes to the status bar height (e.g. via choosing Toggle In-Call Status Bar), among other things, will not push things off the screen.

For reference, here is Apple's official code regarding the transition from one view controller to another:

- (void) cycleFromViewController: (UIViewController*) oldC            toViewController: (UIViewController*) newC{    [oldC willMoveToParentViewController:nil];                        // 1    [self addChildViewController:newC];    newC.view.frame = [self newViewStartFrame];                       // 2    CGRect endFrame = [self oldViewEndFrame];    [self transitionFromViewController: oldC toViewController: newC   // 3          duration: 0.25 options:0          animations:^{             newC.view.frame = oldC.view.frame;                       // 4             oldC.view.frame = endFrame;           }           completion:^(BOOL finished) {             [oldC removeFromParentViewController];                   // 5             [newC didMoveToParentViewController:self];            }];}

Rather than using frames as in the example above, we must add constraints. The question is where to add them. We cannot add them at marker (2) above, since newC.view is not installed in the view hierarchy. It is only installed the moment we call transitionFromViewController... (3). That means we can either install the constraints right after the call to transitionFromViewController, or we can do it as the first line in the animation block. Both should work. If you want to do it at the earliest time, then putting it in the animation block is the way to go. More on the order of how these blocks are called will be discussed below.

In summary, for just positioning via auto layout, use a template such as:

- (void)cycleFromViewController:(UIViewController *)oldViewController               toViewController:(UIViewController *)newViewController{    [oldViewController willMoveToParentViewController:nil];    [self addChildViewController:newViewController];    newViewController.view.alpha = 0;    [self transitionFromViewController:oldViewController                      toViewController:newViewController                              duration:0.25                               options:0                            animations:^{                                newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;                                // create constraints for newViewController.view here                                newViewController.view.alpha = 1;                            }                            completion:^(BOOL finished) {                                [oldViewController removeFromParentViewController];                                [newViewController didMoveToParentViewController:self];                            }];    // or create constraints right here}

Solution 2: Animating constraint changes

Animating constraint changes is not as simple, because we are not given a callback between when the view is attached to the hierarchy and when the animation block is called via the transitionFromViewController... method.

For reference, here is the standard way of adding/removing a child view controller:

- (void) displayContentController: (UIViewController*) content;{   [self addChildViewController:content];                 // 1   content.view.frame = [self frameForContentController]; // 2   [self.view addSubview:self.currentClientView];   [content didMoveToParentViewController:self];          // 3}- (void) hideContentController: (UIViewController*) content{   [content willMoveToParentViewController:nil];  // 1   [content.view removeFromSuperview];            // 2   [content removeFromParentViewController];      // 3}

By comparing these two methods and the original cycleFromViewController: posted above, we see that transitionFromViewController takes care of two things for us:

  • [self.view addSubview:self.currentClientView];
  • [content.view removeFromSuperview];

By adding some logging (omitted from this post), we can get a good idea of when these methods are called.

After doing so, it appears that the method is implemented in a manner similar to the following:

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion{    [self.view addSubview:toViewController.view];  // A    animations();                                  // B    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(duration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{        [fromViewController.view removeFromSuperview];        completion(YES);    });}

Now it is clear to see why it's not possible to use transitionFromViewController to animate constraint changes. The first time you can initialize constraints is after the view is added (line A). The constraints should be animated in the animations() block (line B), but there is no way to run code between these two lines.

Therefore, we must use a manual animation block, along with the standard method of animating constraint changes:

- (void)cycleFromViewController:(UIViewController *)oldViewController               toViewController:(UIViewController *)newViewController{    [oldViewController willMoveToParentViewController:nil];    [self addChildViewController:newViewController];    [self.view addSubview:newViewController.view];    newViewController.view.translatesAutoresizingMaskIntoConstraints = NO;    // TODO: create initial constraints for newViewController.view here    [newViewController.view layoutIfNeeded];    // TODO: update constraint constants here    [UIView animateWithDuration:0.25                     animations:^{                         [newViewController.view layoutIfNeeded];                     }                     completion:^(BOOL finished) {                         [oldViewController.view removeFromSuperview];                         [oldViewController removeFromParentViewController];                         [newViewController didMoveToParentViewController:self];                     }];}

Warnings

This is not equivalent to how the storyboard embeds a container view controller. For example, if you compare the translatesAutoresizingMaskIntoConstraints value of the embedded view via a storyboard vs. the method above, it will report YES for the storyboard, and NO (obviously, since we explicitly set it to NO) for the method I recommend above.

This can lead to inconsistencies in your app, since certain parts of the system seem to depend on UIViewController containment to be used with translatesAutoresizingMaskIntoConstraints set to NO. For example, on an iPad Air (8.4), you may get strange behavior when rotating from portrait to landscape.

The simple solution seems to be to keep translatesAutoresizingMaskIntoConstraints set to NO, then set newViewController.view.frame = newViewController.view.superview.bounds. However, unless you are very careful with when this method is called, it most likely will give you an incorrect visual layout. (Note: The way that the storyboard ensures the view sizes properly is by setting the embedded view's autoresize property to W+H. Printing out the frame right after adding the subview will also reveal a difference between the storyboard vs. programatic approach, which suggests that Apple is setting the frame directly on the contained view.)