How can I implement a swiping/sliding animation between views?

user1368136 picture user1368136 · May 1, 2012 · Viewed 63.7k times · Source

I have a few views between which I want to swipe in an iOS program. Right now, I'm swiping between them using a modal style, with a cross dissolve animation. However, I want to have a swiping/sliding animation like you see on the home screen and such. I have no idea how to code such a transition, and the animation style isn't an available modal transition style. Can anyone give me an example of the code? It doesn't need to be a modal model or anything, I just found that easiest.

Answer

Rob picture Rob · May 2, 2012

Since iOS 7, if you want to animate the transition between two view controllers, you would use custom transitions, as discussed in WWDC 2013 video Custom Transitions Using View Controllers. For example, to customize the presentation of a new view controller you would:

  1. The destination view controller would specify the self.modalPresentationStyle and transitioningDelegate for the presentation animation:

    - (instancetype)initWithCoder:(NSCoder *)coder {
        self = [super initWithCoder:coder];
        if (self) {
            self.modalPresentationStyle = UIModalPresentationCustom;
            self.transitioningDelegate = self;
        }
        return self;
    }
    
  2. This delegate (in this example, the view controller itself) would conform to UIViewControllerTransitioningDelegate and implement:

    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
                                                                       presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        return [[PresentAnimator alloc] init];
    }
    
    // in iOS 8 and later, you'd also specify a presentation controller
    
    - (UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
        return [[PresentationController alloc] initWithPresentedViewController:presented presentingViewController:presenting];
    }
    
  3. You would implement an animator that would perform the desired animation:

    @interface PresentAnimator : NSObject <UIViewControllerAnimatedTransitioning>
    
    @end
    
    @implementation PresentAnimator
    
    - (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
        return 0.5;
    }
    
    // do whatever animation you want below
    
    - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
        UIViewController* toViewController   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
        UIViewController* fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    
        [[transitionContext containerView] addSubview:toViewController.view];
        CGFloat width = fromViewController.view.frame.size.width;
        CGRect originalFrame = fromViewController.view.frame;
        CGRect rightFrame = originalFrame; rightFrame.origin.x += width;
        CGRect leftFrame  = originalFrame; leftFrame.origin.x  -= width / 2.0;
        toViewController.view.frame = rightFrame;
    
        toViewController.view.layer.shadowColor = [[UIColor blackColor] CGColor];
        toViewController.view.layer.shadowRadius = 10.0;
        toViewController.view.layer.shadowOpacity = 0.5;
    
        [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
            fromViewController.view.frame = leftFrame;
            toViewController.view.frame = originalFrame;
            toViewController.view.layer.shadowOpacity = 0.5;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    
    @end
    
  4. You'd also implement a presentation controller that takes care of cleaning up the view hierarchy for you. In this case, since we're completely overlaying the presenting view, we can remove it from the hierarchy when the transition is done:

    @interface PresentationController: UIPresentationController
    @end
    
    @implementation PresentationController
    
    - (BOOL)shouldRemovePresentersView {
        return true;
    }
    
    @end
    
  5. Optionally, if you wanted this gesture to be interactive, you would also:

    • Create an interaction controller (typically a UIPercentDrivenInteractiveTransition);

    • Have your UIViewControllerAnimatedTransitioning also implement interactionControllerForPresentation, which obviously would return the aforementioned interaction controller;

    • Have a gesture (or what have you) that updates the interactionController

This is all described in the aforementioned Custom Transitions Using View Controllers.

For example of customizing navigation controller push/pop, see Navigation controller custom transition animation


Below, please find a copy of my original answer, which predates custom transitions.


@sooper's answer is correct, that CATransition can yield the effect you're looking for.

But, by the way, if your background isn't white, the kCATransitionPush of CATransition has an weird fade in and fade out at the end of the transition that can be distracting (when navigating between images, especially, it lends it a slightly flickering effect). If you suffer from this, I found this simple transition to be very graceful: You can prepare your "next view" to be just off screen to the right, and then animate the moving of the current view off screen to the left while you simultaneously animate the next view to move to where the current view was. Note, in my examples, I'm animating subviews in and out of the main view within a single view controller, but you probably get the idea:

float width = self.view.frame.size.width;
float height = self.view.frame.size.height;

// my nextView hasn't been added to the main view yet, so set the frame to be off-screen

[nextView setFrame:CGRectMake(width, 0.0, width, height)];

// then add it to the main view

[self.view addSubview:nextView];

// now animate moving the current view off to the left while the next view is moved into place

[UIView animateWithDuration:0.33f 
                      delay:0.0f 
                    options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionAllowUserInteraction
                 animations:^{
                     [nextView setFrame:currView.frame];
                     [currView setFrame:CGRectMake(-width, 0.0, width, height)];
                 }
                 completion:^(BOOL finished){
                     // do whatever post processing you want (such as resetting what is "current" and what is "next")
                 }];

Clearly, you'd have to tweak this for how you've got your controls all set up, but this yields a very simple transition, no fading or anything like that, just a nice smooth transition.

A caveat: First, neither this example, nor the CATransition example, are quite like the SpringBoard home screen animation (that you talked about), which is continuous (i.e. if you're half way through a swipe, you can stop and go back or whatever). These transitions are ones that once to initiate them, they just happen. If you need that realtime interaction, that can be done, too, but it's different.

Update:

If you want to use a continuous gesture that tracks the user's finger you can use UIPanGestureRecognizer rather than UISwipeGestureRecognizer, and I think animateWithDuration is better than CATransition in that case. I modified my handlePanGesture to change the frame coordinates to coordinate with the user's gesture, and then I modified the above code to just complete the animation when the user let go. Works pretty well. I don't think you can do that with CATransition very easily.

For example, you might create a gesture handler on the controller's main view:

[self.view addGestureRecognizer:[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)]];

And the handler might look like:

- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
    // transform the three views by the amount of the x translation

    CGPoint translate = [gesture translationInView:gesture.view];
    translate.y = 0.0; // I'm just doing horizontal scrolling

    prevView.frame = [self frameForPreviousViewWithTranslate:translate];
    currView.frame = [self frameForCurrentViewWithTranslate:translate];
    nextView.frame = [self frameForNextViewWithTranslate:translate];

    // if we're done with gesture, animate frames to new locations

    if (gesture.state == UIGestureRecognizerStateCancelled ||
        gesture.state == UIGestureRecognizerStateEnded ||
        gesture.state == UIGestureRecognizerStateFailed)
    {
        // figure out if we've moved (or flicked) more than 50% the way across

        CGPoint velocity = [gesture velocityInView:gesture.view];
        if (translate.x > 0.0 && (translate.x + velocity.x * 0.25) > (gesture.view.bounds.size.width / 2.0) && prevView)
        {
            // moving right (and/or flicked right)

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 prevView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }
                             completion:^(BOOL finished) {
                                 // do whatever you want upon completion to reflect that everything has slid to the right

                                 // this redefines "next" to be the old "current",
                                 // "current" to be the old "previous", and recycles
                                 // the old "next" to be the new "previous" (you'd presumably.
                                 // want to update the content for the new "previous" to reflect whatever should be there

                                 UIView *tempView = nextView;
                                 nextView = currView;
                                 currView = prevView;
                                 prevView = tempView;
                                 prevView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                             }];
        }
        else if (translate.x < 0.0 && (translate.x + velocity.x * 0.25) < -(gesture.view.frame.size.width / 2.0) && nextView)
        {
            // moving left (and/or flicked left)

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 nextView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                             }
                             completion:^(BOOL finished) {
                                 // do whatever you want upon completion to reflect that everything has slid to the left

                                 // this redefines "previous" to be the old "current",
                                 // "current" to be the old "next", and recycles
                                 // the old "previous" to be the new "next". (You'd presumably.
                                 // want to update the content for the new "next" to reflect whatever should be there

                                 UIView *tempView = prevView;
                                 prevView = currView;
                                 currView = nextView;
                                 nextView = tempView;
                                 nextView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }];
        }
        else
        {
            // return to original location

            [UIView animateWithDuration:0.25
                                  delay:0.0
                                options:UIViewAnimationOptionCurveEaseOut
                             animations:^{
                                 prevView.frame = [self frameForPreviousViewWithTranslate:CGPointZero];
                                 currView.frame = [self frameForCurrentViewWithTranslate:CGPointZero];
                                 nextView.frame = [self frameForNextViewWithTranslate:CGPointZero];
                             }
                             completion:NULL];
        }
    }
}

That uses these simple frame methods that you'd presumably define for your desired UX:

- (CGRect)frameForPreviousViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(-self.view.bounds.size.width + translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

- (CGRect)frameForCurrentViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

- (CGRect)frameForNextViewWithTranslate:(CGPoint)translate
{
    return CGRectMake(self.view.bounds.size.width + translate.x, translate.y, self.view.bounds.size.width, self.view.bounds.size.height);
}

Your particular implementation will undoubtedly vary, but hopefully this illustrates the idea.

Having illustrated all of this (supplementing and clarifying this old answer), I should point out that I don't use this technique any more. Nowadays, I generally use a UIScrollView (with "paging" turned on) or (in iOS 6) a UIPageViewController. This gets you out of the business of writing this sort of gesture handler (and enjoying extra functionality like scroll bars, bouncing, etc.). In the UIScrollView implementation, I just respond to the scrollViewDidScroll event to make sure that I'm lazily loading the necessary subview.