UIViewController – issue with custom dismiss transition
Asked Answered
K

2

28

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:

  • I've changed its view's frame.
  • I've changed its view's transform.
  • I've changed its view's layer's 3d transform.

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!

Kucera answered 6/6, 2014 at 15:46 Comment(0)
P
41

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

  • Add the presented ("to") view to the container, then bring the presented view front. Don't add the presenting view as you might remove it from its current superview.
  • On dismiss, UIKit sets the alpha of the presented view to 0 before animateTransition is called. So you'll want to set it to 1.0 or whatever it was at the completion of the present before firing off your dismiss animation(s).
  • Likewise for the presented view's transform. On dismiss it gets reset to identity before your animateTransition is called.

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]];
        }];
    }
}
Phelan answered 12/6, 2014 at 3:50 Comment(24)
Superb, John – that sounds very promising, thank you. I'll try that today I hope. Are the gotchas that you mention documented – I'll add a link if they are.Kucera
Incidentally, the second and last gotchas just seem weird! I wonder if they are bugs? I speculated that possibly I wasn't cleaning up my animation properly and so things were being reset to their initial state before the present for a reason I didn't understand, giving the effect of a "reset".Kucera
Gotcha #1 isn't documented beyond "The view of the currently active view controller is likely already a subview of this container view but the view belonging to the view controller being presented may need to be added.". I just find it to be good practice. I suspect the other two are UIKit bugs and I've filed rdar://17295478 but haven't heard anything back. I wonder if this is different with the iOS 8 SDK?Phelan
Apple replied back that the bug was fixed in iOS 8 seed 2 SDK. I checked and sure enough it appears to be. (Looks like a new bug was introduced where the screen turns white afterward; I filed a new radar for that.) So I think you'll want the "pre-transform" workaround code to only execute on iOS 7.Phelan
Nice one John – thank's for letting me know. I'll definitely test on 8, then.Kucera
I'm having this exact same issue with a custom modal presentation. Where can I set the presented view controller's transform just prior to animateTransition: being called?Milinda
> "Looks like a new bug was introduced where the screen turns white afterward; I filed a new radar for that." I am struggling with a white screen upon dismissal of some custom transition animations. I'm seeing this on iOS 8 beta 5. @JohnScalo have you heard anything back from Apple?Oniskey
1. UIKit does not change alpha value. 2. I experience blank screen after dismiss finished. 3. If I do not remove presenting VC view when presenting, then unwinding does not work.Dolt
I found that setting the to be presented view controller's modalPresentationStyle to UIModalPresentationCustom is causing the white view on dismissal (iOS 8 only). If you are having this issue, try commenting out viewController.modalPresentationStyle = UIModalPresentationCustom;Oniskey
@Oniskey The bug is still open (a good sign) but with no activity visible to me. The radar # is 17476279 and the title is "Dismissing with custom transitions results in white screen", in case that helps.Phelan
JohnScalo I'm having the same bug yep. The workaround from @Oniskey worked for me so I'm using it as long as it's not fixed on Apple side.Seedy
@JohnScalo Is this bug still open? Wondering if we'll see a fix in the GM.Larisalarissa
17295478 (the bug this was originally about) is closed but 17476279 (white screen afterward) is still open. That said, I don't see the white screen problem any longer.Phelan
@JohnScalo Unfortunately the bug is still very present for me in the GM, reproducible by having a custom dismissal transition and setting modalPresentationStyle to UIModalPresentationCustom.Larisalarissa
I just filed rdar://18300812 along with a full test project. If anyone is interested in seeing the test project, let me know.Larisalarissa
I'm using pre release Xcode version, still white screen. Neither options are helpful. Sad...Scrawny
Guys, for those who had black/white screen after animateTransition: (on dismiss). I finally found the reason. Do not comment out vc.modalPresentationStyle = UIModalPresentationCustom. The problem was in that line: [transitionContext.containerView addSubview:fromViewController.view]. Don't re-add the 'from' (UITransitionContextFromViewControllerKey) view again since it's in view hierarchy already. It was fine on iOS7 but not in iOS8.Ulises
Just improving on SoftDesigner's answer, all you have to do is [transitionContext.containerView addSubview:toViewController.view] when you are presenting. No need to add it again when dismissing. No need also to add fromViewController.view when presenting/dismissing. It works on iOS 7+Slipknot
No joy over here. If I don't add both the presenting and presented views to the context's container view, my presenting transition "fails". The presenting view is completely removed and the presented view appears with the wrong alpha. If I do add them both, then after the dismissing, the screen goes black. If I interrupt the debugger and po [[[[UIApplication sharedApplication] windows] objectAtIndex: 0] recursiveDescription] LLDB shows me that the hierarchy is completely empty – my view controllers have all been thrown away.Kucera
Maybe some joy :-) … When I leave out adding the presenting controller, what happens makes sense, but the presented controller is in front of the presenting controller. I need it to be behind the presenting controller.Kucera
Nope – no joy. If I don't add the presenting controller then the animation that I apply to it (the translate down by 400 pixels) completes immediately rather than being interactive :-(Kucera
I'm giving up. I managed to get the presented controller behind the presenting one, as desired, by placing [[containerView superview] sendSubviewToBack: containerView] in the presenting code. However, the animation on the presenting view controller runs immediately, not under interactive control. I tried using [presentingController transitionCoordinator] animateAlongsideTransition:… to rectify this, but this animation is still completed immediately. My current view is that the API is not intended for also animating the presenting controller.Kucera
Just a note about checking if a controller is presenting or dismissing, you can also get if the destination is being by using its method [destination isBeingPresented]Hoyos
@dccarmo you made my day! That's ITSuzettesuzi
Q
0

Initially, I thought about using CATransition to have custom transition effect when presentViewController:animated:completion: and dismissViewControllerAnimated:completion: a View Controller. But you want to show a portion of View Controller when the setting View Controller is presented, then I think CATransition would not help because you don't have full control of how far you want to move the View Controller.

I think the easiest way is to have one single View Controller with two full screen UIView. For the first UIView (View Controller's view, that is, self.view), you layout the setting, and on the second UIView, it's the regular View. In the ViewDidLoad, you add the 2nd view by using [self.view addSubview:2ndView];. Later when you want to present the setting view, you can do

CGRect frame = secondView.frame;
frame.origin.y = the_y_coordinate_you_like;
UIView animateWithDuration:0.2 animations:^{
    secondView.frame = frame;
}];

then do the other way to bring the 2ndView back.

Quizmaster answered 6/6, 2014 at 16:54 Comment(6)
Thanks for your answer, but I don't understand your claim that a CCTransition doesn't give full control if how far the controller is moved. Is this mentioned in the docs, etc?Kucera
CATransition is to control how the transition should be, like moving to right or left, fade-in or fade-out, transition speed. I don't see how it can control the distance you want to move the view, maybe I missed something.Quizmaster
Ah, okay – Apple added a really significant library to support view controller transitions in iOS 7. It's suppose to do exactly this kind of thing. Here's a reasonable introduction.Kucera
I am not familiar with UIViewControllerContextTransitioning, but it looks different from CATransition. The sample code from the link also uses UIView animateWithDuration:animations: to achieve the animation effect.Quizmaster
This is the way I meant to use CATransition to change the transition style when present or hide a View Controller with presentViewController:animated:completion: or dismissViewControllerAnimated:completion:, iPhone - designing my own viewController transition .Quizmaster
nods Yes. The transitioning API lets you use any kind of UIView animation (as you discuss in your answer) to transition UIViewContoroller presents and dismiss operations. It's pretty sweet, but odd that this doesn't quite seem to work.Kucera

© 2022 - 2024 — McMap. All rights reserved.