How can I implement a swiping/sliding animation between views?
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.
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:
-
The destination view controller would specify the
self.modalPresentationStyle
andtransitioningDelegate
for the presentation animation:- (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self) { self.modalPresentationStyle = UIModalPresentationCustom; self.transitioningDelegate = self; } return self; }
-
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]; }
-
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
-
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
-
Optionally, if you wanted this gesture to be interactive, you would also:
Create an interaction controller (typically a
UIPercentDrivenInteractiveTransition
);Have your
UIViewControllerAnimatedTransitioning
also implementinteractionControllerForPresentation
, 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.
You could create a CATransition
animation. Here's an example of how you can slide a second view (from left) into the screen whilst pushing the current view out:
UIView *theParentView = [self.view superview];
CATransition *animation = [CATransition animation];
[animation setDuration:0.3];
[animation setType:kCATransitionPush];
[animation setSubtype:kCATransitionFromLeft];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[theParentView addSubview:yourSecondViewController.view];
[self.view removeFromSuperview];
[[theParentView layer] addAnimation:animation forKey:@"showSecondViewController"];