iosuiviewcontrolleruikituiviewanimationtransition

UIViewController – issue with custom dismiss transition


Summary

I have a content UIViewController that presents a settings UIViewController using a custom transition. The presentation is with presentViewController:animated:completion:.

When I later dismiss the settings with dismissViewControllerAnimated:completion:, the presenting controller is suddenly jumped back to it's initial position prior to the settings controller presentation.

I have a work around for this on the device but not the simulator. However, I'd like to know what I'm doing wrong rather than hack in a bodge that makes it go away. I also plan to make this animation interactive, and I suspect this issues will amplify when I do this.

Custom Transition – Opening the hood

The desired effect is that the presenting controller slides down the screen, and the presented controller is seen to be lying behind it from where it lifts up to fill the screen. The top of the presenting controller remains on-screen during the lifetime of use of the presented controller. It stays at the bottom of the screen, but above the presented controller.

You could imagine lifting the bonnet on a car (the front presenting controller) to see the engine behind (the presented settings), but the bonnet stays visible at the bottom for a bit of context.

I plan to refine this so that the presenting controller really appears to lift up with perspective in a 3d way, but I've not got that far, yet.

When the settings are dismissed, the original presenting controller (bonnet) should slide back up the screen and the presented controller (settings) sink back slightly (closing the bonnet).

Code

Here's the method that toggles the settings on and off the screen (it's just called by a UIButton). You'll notice that the presenting view controller sets itself up as the <UIViewControllerTransitioningDelegate>.

-(void) toggleSettingsViewController
{
  const BOOL settingsAreShowing = [self presentedViewController] != nil;
  if(!settingsAreShowing)
  {
    UIViewController *const settingsController = [[self storyboard] instantiateViewControllerWithIdentifier: @"STSettingsViewController"];
    [settingsController setTransitioningDelegate: self];
    [settingsController setModalPresentationStyle: UIModalPresentationCustom];
    [self presentViewController: settingsController animated: YES completion: nil];
  }
  else
  {
    [self dismissViewControllerAnimated: YES completion: nil];
  }
}

To implement <UIViewControllerAnimatedTransitioning> the presenting view controller also just returns itself as the <UIViewControllerAnimatedTransitioning>

-(id<UIViewControllerAnimatedTransitioning>) animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
  return self;
}

-(id<UIViewControllerAnimatedTransitioning>) animationControllerForDismissedController:(UIViewController *)dismissed
{
  // Test Point 1.
  return self;
}

So finally, the presenting view controller will receive animateTransition::

-(void) animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
  UIViewController *const fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
  UIViewController *const toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

  const BOOL isUnwinding = [toController presentedViewController] == fromController;
  const BOOL isPresenting = !isUnwinding;

  UIViewController * presentingController = isPresenting ? fromController : toController;
  UIViewController * presentedController = isPresenting ? toController : fromController;

  if(isPresenting)
  {
    // Add the presented controller (settings) to the view hierarchy _behind_ the presenting controller.
    [[transitionContext containerView] insertSubview: [presentedController view] belowSubview: [presentingController view]];

    // Set up the initial position of the presented settings controller. Scale it down so it seems in the distance. Alpha it down so it is dark and shadowed.
    presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
    presentedController.view.alpha = 0.7;

    [UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
      // Lift up the presented controller.
      presentedController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);

      // Brighten the presented controller (out of shadow).
      presentedController.view.alpha = 1;

      // Push the presenting controller down the screen – 3d effect to be added later.
      presentingController.view.layer.transform = CATransform3DMakeTranslation(0,400,0);
     } completion: ^(BOOL finished){
       [transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
     }];
  }
  else
  {
    // Test Point 2.

    // !!!This line should not be needed!!!
    // It resets the presenting controller to where it ought to be anyway.
    presentingController.view.layer.transform = CATransform3DMakeTranslation(0,400,0);

    [UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
      // Bring the presenting controller back to its original position.
      presentingController.view.layer.transform = CATransform3DIdentity;

      // Lower the presented controller again and put it back in to shade.
      presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
      presentedController.view.alpha = 0.4;
    } completion:^(BOOL finished) {
      [transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
    }];
  }
}

-(NSTimeInterval) transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
  return 0.5;
}

Problem

In the code above, I've indicated !!!This line should not be needed!!!.

What's happening is that between Test Point 1 and Test Point 2 the screen position of the presenting view controller is reset to be the default full screen bounds. So, instead of being at the bottom of the screen ready to animate back up again smoothly, it suddenly jumps up the screen to position that it is meant to smoothly animate too!

I've tried various approaches to animating the presenting view controller down the screen:

In all cases, at Test Point 1, when the transition delegate is asked for, the presenting controller is set up as I would expect. However, in all cases, at Test Point 2, the presenting view controller has lost the correct position and has been "cleared" to have the normal full screen position that I want to animate it to.

In the work around above I explicitly relocate the presenting view controller back to where it should be at the start of the animation with !!!This line should not be needed!!!. This seems to work on the device with the current version of iOS 7. However, on the simulator, the controller is visible at the cleared position for at least one frame.

I am suspicious that I am doing something else wrong, and that I'm going to get in to trouble with my workaround just masking another problem.

Any ideas what's going on? Thanks!


Solution

  • A few potential gotchas with dismissal of modally presented view controllers using custom transition animations:

    Given all that, I think this should work:

    -(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
    {
        UIViewController *fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
        UIViewController *toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        UIView *containerView = transitionContext.containerView;
        
        const BOOL isUnwinding = [toController presentedViewController] == fromController;
        const BOOL isPresenting = !isUnwinding;
        
        UIViewController *presentingController = isPresenting ? fromController : toController;
        UIViewController *presentedController = isPresenting ? toController : fromController;
        
        [containerView addSubview:presentingController.view];
        [containerView bringSubviewToFront:presentingController.view];
        
        if(isPresenting)
        {
            // Set up the initial position of the presented settings controller. Scale it down so it seems in the distance. Alpha it down so it is dark and shadowed.
            presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
            presentedController.view.alpha = 0.7;
            
            [UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
                // Lift up the presented controller.
                presentedController.view.transform = CGAffineTransformMakeScale(1.0, 1.0);
                
                // Brighten the presented controller (out of shadow).
                presentedController.view.alpha = 1;
                
                // Push the presenting controller down the screen – 3d effect to be added later.
                presentingController.view.layer.transform = CATransform3DMakeTranslation(0,400,0);
            } completion: ^(BOOL finished){
                [transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
            }];
        }
        else
        {
            presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
            presentedController.view.alpha = 0.7;
            
            [UIView animateWithDuration: [self transitionDuration: transitionContext] animations:^{
                // Bring the presenting controller back to its original position.
                presentingController.view.layer.transform = CATransform3DIdentity;
                
                // Lower the presented controller again and put it back in to shade.
                presentedController.view.transform = CGAffineTransformMakeScale(0.9, 0.9);
                presentedController.view.alpha = 0.4;
            } completion:^(BOOL finished) {
                [transitionContext completeTransition: ![transitionContext transitionWasCancelled]];
            }];
        }
    }