Interactive View Controller Transitions

Episode #88 | 18 minutes | published on September 26, 2013
Subscribers Only
In this episode we continue our transition example from episode 86. We create a new InteractiveSwipe transition class that we can use for dismissal so the user can swipe the view controllers away instead of tapping a button.

Episode Links

Creating the new Interactive Swipe

You can do this in the same transition class if you like, but I chose to create a separate object.


@interface InteractiveSwipe : UIPercentDrivenInteractiveTransition
@property (nonatomic, strong) UIViewController *viewController;
@property (nonatomic, strong) UIPanGestureRecognizer *pan;
- (void)attachToViewController:(UIViewController *)viewController;
@end
#import "InteractiveSwipe.h"

@interface InteractiveSwipe () {
    BOOL _shouldComplete;
}

@end

@implementation InteractiveSwipe

- (void)attachToViewController:(UIViewController *)viewController {
    self.viewController = viewController;
    self.pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(onPan:)];
    [self.viewController.view addGestureRecognizer:self.pan];
}

- (void)onPan:(UIPanGestureRecognizer *)pan {
    NSAssert(self.viewController != nil, @"view controller was not set");
    CGPoint translation = [pan translationInView:pan.view.superview];

    switch (pan.state) {
        case UIGestureRecognizerStateBegan:
            [self.viewController dismissViewControllerAnimated:YES completion:nil];
            break;

        case UIGestureRecognizerStateChanged: {
            const CGFloat DragAmount = 200;
            const CGFloat Threshold = 0.5;
            CGFloat percent = translation.x / DragAmount;
            percent = fmaxf(percent, 0.0);
            percent = fminf(percent, 1.0);
            [self updateInteractiveTransition:percent];

            _shouldComplete = percent >= Threshold;
            break;
        }

        case UIGestureRecognizerStateEnded:
        case UIGestureRecognizerStateCancelled: {
            if (pan.state == UIGestureRecognizerStateCancelled || !_shouldComplete) {
                [self cancelInteractiveTransition];
            } else {
                [self finishInteractiveTransition];
            }
            break;
        }


        default:
            break;
    }
}

- (CGFloat)completionSpeed {
    return 1 - self.percentComplete;
}

Here we include a method for attaching to a view controller. We need this because we don't have an easy way of getting the view controller that we're dismissing from the TransitionList view controller where we need to return this object, so this method allows the view controller to be wired up elsewhere.

Once the pan gesture recognizer is in place we just need to handle the events. We determine if the transition should be completed or not and we call the relevant finish method on the super class when the gesture ends or is cancelled.

Attaching the interaction object

We add a method in viewDidAppear: to set up the gesture recognizer:


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self wireUpInteraction];
}

- (void)wireUpInteraction {
    id animator = [self.transitioningDelegate animationControllerForDismissedController:self];
    id interactor = [self.transitioningDelegate interactionControllerForDismissal:animator];

    if ([interactor respondsToSelector:@selector(attachToViewController:)]) {
        [interactor attachToViewController:self];
    }
}

Next, we need to update our transition class.

Updating the transition class to deal with viewDidLoad and failure

        [toVC viewWillAppear:YES];
        [UIView animateWithDuration:SWATCH_DISMISS_DURATION
                              delay:0
                            options:UIViewAnimationOptionCurveEaseIn
                         animations:^{
                             fromVC.view.transform = rotation;
                         } completion:^(BOOL finished) {
                             if ([transitionContext transitionWasCancelled]) {
                                 [transitionContext completeTransition:NO];
                             } else {
                                 [fromVC.view removeFromSuperview];
                                 [toVC viewDidAppear:YES];
                                 [transitionContext completeTransition:YES];
                             }
                         }];
    }

Note that we have to manually call viewWillAppear, viewDidAppear so that our view controllers wire themselves up to the interactor properly. I suspect this isn't an issue when customizing navigation or tab bar based transitions.

Lastly, we handle the case where the transition was cancelled so we don't remove the view from the hierarchy.

blog comments powered by Disqus