How to complete interactive UIViewController transition?

DanielRak picture DanielRak · Oct 1, 2013 · Viewed 12.5k times · Source

I've been dabbling with the new iOS 7 custom transition API and looked through all the tutorials/documentation I could find but I can't seem to figure this stuff out for my specific scenario.

So essentially what I'm trying to implement is a UIPanGestureRecognizer on a view where I would swipe up and transition to a VC whose view would slide up from the bottom while the current view would slide up as I drag my finger higher.

I have no problem accomplishing this without the interaction transition, but once I implement the interaction (the pan gesture) I can't seem to complete the transition.

Here's the relevant code from the VC that conforms to the UIViewControllerTransitionDelegate which is needed to vend the animator controllers:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.identifier isEqualToString:@"Swipe"]) {
        NSLog(@"PREPARE FOR SEGUE METHOD CALLED");

        UIViewController *toVC = segue.destinationViewController;
        [interactionController wireToViewController:toVC];
        toVC.transitioningDelegate = self;
        toVC.modalPresentationStyle = UIModalPresentationCustom;
      }
}

#pragma mark UIViewControllerTransition Delegate Methods

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:  (UIViewController *)presented                                                                   presentingController:  (UIViewController *)presenting sourceController:(UIViewController *)source {
    NSLog(@"PRESENTING ANIMATION CONTROLLER CALLED");

    SwipeDownPresentationAnimationController *transitionController = [SwipeDownPresentationAnimationController new];
    return transitionController;
}

- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    NSLog(@"DISMISS ANIMATION CONTROLLER CALLED");

    DismissAnimatorViewController *transitionController = [DismissAnimatorViewController new];
    return transitionController;
}

- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
    NSLog(@"Interaction controller for dimiss method caled");

    return interactionController.interactionInProgress ? interactionController:nil;
}

NOTE: The interaction swipe is only for the dismissal of the VC which is why it's in the interactionControllerForDismissal method

Here's the code for the animator of the dismissal which works fine when I tap on a button to dismiss it:

#import "DismissAnimatorViewController.h"

@implementation DismissAnimatorViewController

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

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    NSTimeInterval duration = [self transitionDuration:transitionContext];

    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

    CGRect initialFrameFromVC = [transitionContext initialFrameForViewController:fromVC];

    UIView *containerView = [transitionContext containerView];

    CGRect screenBounds = [[UIScreen mainScreen] bounds];
    NSLog(@"The screen bounds is :%@", NSStringFromCGRect(screenBounds));
    toVC.view.frame = CGRectOffset(initialFrameFromVC, 0, screenBounds.size.height);
    toVC.view.alpha = 0.2;

    CGRect pushedPresentingFrame = CGRectOffset(initialFrameFromVC, 0, -screenBounds.size.height);

    [containerView addSubview:toVC.view];

    [UIView animateWithDuration:duration
                          delay:0
         usingSpringWithDamping:0.6
          initialSpringVelocity:0
                        options:UIViewAnimationOptionCurveEaseIn
                     animations:^{
                         fromVC.view.frame = pushedPresentingFrame;
                         fromVC.view.alpha = 0.2;
                         toVC.view.frame = initialFrameFromVC;
                         toVC.view.alpha = 1.0;
                     } completion:^(BOOL finished) {
                         [transitionContext completeTransition:YES];
                     }];
}

@end

Here's the code for the UIPercentDrivenInteractiveTransition subclass which serves as the interaction controller:

#import "SwipeInteractionController.h"

@implementation SwipeInteractionController {
    BOOL _shouldCompleteTransition;
    UIViewController *_viewController;
}

- (void)wireToViewController:(UIViewController *)viewController {
    _viewController = viewController;
    [self prepareGestureRecognizerInView:_viewController.view];
}

- (void)prepareGestureRecognizerInView:(UIView*)view {
    UIPanGestureRecognizer *gesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
    gesture.minimumNumberOfTouches = 1.0;

    [view addGestureRecognizer:gesture];
}

- (CGFloat)completionSpeed {
    return 1 - self.percentComplete;
    NSLog(@"PERCENT COMPLETE:%f",self.percentComplete);
}

- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer {
//    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view.superview];

    switch (gestureRecognizer.state) {
        case UIGestureRecognizerStateBegan:
            // 1. Start an interactive transition!
            self.interactionInProgress = YES;
            [_viewController dismissViewControllerAnimated:YES completion:nil];
            break;
        case UIGestureRecognizerStateChanged: {
            // 2. compute the current position
            CGFloat fraction = fabsf(translation.y / 568);
            NSLog(@"Fraction is %f",fraction);
            fraction = fminf(fraction, 1.0);
            fraction = fmaxf(fraction, 0.0);
            // 3. should we complete?
            _shouldCompleteTransition = (fraction > 0.23);
            // 4. update the animation controller
            [self updateInteractiveTransition:fraction];
            NSLog(@"Percent complete:%f",self.percentComplete);
            break;
        }
        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            // 5. finish or cancel
            NSLog(@"UI GESTURE RECOGNIZER STATE CANCELED");
            self.interactionInProgress = NO;
            if (!_shouldCompleteTransition || gestureRecognizer.state == UIGestureRecognizerStateCancelled) {
                [self cancelInteractiveTransition];
                NSLog(@"Interactive Transition is cancled.");
                }
            else {
                NSLog(@"Interactive Transition is FINISHED");
                [self finishInteractiveTransition];
            }
            break;
        }
        default:
            NSLog(@"Default is being called");
            break;
    }
}

@end

Once again, when I run the code now and I don't swipe all the way to purposefully cancel the transition, I just get a flash and am presented with the view controller I want to swipe to. This happens regardless if the transition completes or is canceled.

However, when I dismiss via the button I get the transition specified in my animator view controller.

Answer

ColinE picture ColinE · Oct 2, 2013

I can see a couple of issues here - although I cannot be certain that these will fix your problem!

Firstly, your animation controller's UIView animation completion block has the following:

[transitionContext completeTransition:YES];

Whereas it should return completion based on the result of the interaction controller as follows:

[transitionContext completeTransition:![transitionContext transitionWasCancelled]]

Also, I have found that if you tell the UIPercentDrivenInteractiveTransition that a transition is 100% complete, it does not call the animation controller completion block. As a workaround, I limit it to ~99.9%

https://github.com/ColinEberhardt/VCTransitionsLibrary/issues/4

I've created a number of example interaction and animation controllers here, that you might find useful:

https://github.com/ColinEberhardt/VCTransitionsLibrary