How To Make A UIViewController Transition Animation Like in the Ping App
iOS supports custom transitions between view controllers. In this tutorial you’ll implement a UIViewController transition animation like the Ping app. By Luke Parham.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make A UIViewController Transition Animation Like in the Ping App
25 mins
- Strategizing
- Getting Started
- Navigation Controller Delegates
- The UIViewControllerAnimatedTransitioning Protocol
- The CircleTransitionable Protocol
- Animating the Old Text Away
- Fixing the Background
- The Circular Mask Animation
- CAShapeLayer
- CALayer Masking
- Animations with Core Animation
- The Finishing Touches
- Where to Go From Here?
The CircleTransitionable Protocol
If you’ve ever tried writing one of these transitions, you’ve probably figured out that it’s really easy to write code that digs into internal view controller state and feels generally “smelly”. Instead, you’ll define exactly what the view controller needs to provide up front and let any view controller that wishes to animate this way provide access to these views.
Add the following protocol definition at the top of CircularTransition.swift, before the class definition:
protocol CircleTransitionable {
var triggerButton: UIButton { get }
var contentTextView: UITextView { get }
var mainView: UIView { get }
}
This protocol defines the information you’ll need from each view controller in order to successfully animate things.
- The
triggerButton
will be the button the user tapped. - The
contentTextView
will be the text view to animate on or offscreen. - The
mainView
will be the main view to animate on or offscreen.
Next, go to ColoredViewController.swift and make it conform to your new protocol by replacing the definition with the following.
class ColoredViewController: UIViewController, CircleTransitionable {
Luckily, this view controller already defines both the triggerButton
and contentTextView
so it’s already close to ready. The last thing you’ll need to do is add a computed property for the mainView
property. Add the following immediately after the definition of contentTextView
:
var mainView: UIView {
return view
}
Here, all you had to do was return the default view
property of the view controller.
The project contains a BlackViewController
and WhiteViewController
that display the two views in the app. Both are subclasses of ColoredViewController
so you’ve officially set up both classes to be transitionable. Congrats!
Animating the Old Text Away
At long last, it’s time to do some actual animating!
🎉🎉🎉🎉
Navigate back to CircularTransition.swift and add the following guard
statement to animateTransition(transitionContext:)
.
guard let fromVC = transitionContext.viewController(forKey: .from) as? CircleTransitionable,
let toVC = transitionContext.viewController(forKey: .to) as? CircleTransitionable,
let snapshot = fromVC.mainView.snapshotView(afterScreenUpdates: false) else {
transitionContext.completeTransition(false)
return
}
Here you’re making sure you have access to all the major pieces of the puzzle. The transitionContext
allows you to grab references to the view controllers you’re transitioning between. You cast them to CircleTransitionable
so you can later access their main views and text views.
snapshotView(afterScreenUpdates:)
returns a snapshotted bitmap of fromVC
.
Snapshot views are a really useful way to quickly grab a disposable copy of a view for animations. You can’t animate individual subviews around, but if you just need to animate an entire hierarchy without having to put things back when you’re done then a snapshot is an ideal solution.
In the else
clause of your guard you’re calling completeTransition()
on the transitionContext
. You pass false
to tell UIKit that you didn’t complete the transition and that it shouldn’t move to the next view controller.
After the guard
, grab a reference to the container view the context provides.
let containerView = transitionContext.containerView
This view is like your scratchpad for adding and removing views on the way to your final destination.
When you’re done animating, you’ll have done the following in containerView
:
- Removed the
fromVC
‘s view from the container. - Added the
toVC
‘s view to the destination with subviews configured as they should be on appearance.
Add the following at the bottom of animateTransition(transitionContext:)
:
containerView.addSubview(snapshot)
To animate the old text offscreen without messing up the actual text view’s frame, you’ll animate a the snapshot
.
Next, remove the actual view you’re coming from since you won’t be needing it anymore.
fromVC.mainView.removeFromSuperview()
Finally, add the following animation method below animateTransition(transitionContext:)
.
func animateOldTextOffscreen(fromView: UIView) {
// 1
UIView.animate(withDuration: 0.25,
delay: 0.0,
options: [.curveEaseIn],
animations: {
// 2
fromView.center = CGPoint(x: fromView.center.x - 1300,
y: fromView.center.y + 1500)
// 3
fromView.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
}, completion: nil)
}
This method is pretty straightforward:
- You define an animation that will take
0.25
seconds to complete and eases into its animation curve. - You animate the view’s center down and to the left offscreen.
- The view is blown up by 5x so the text seems to grow along with the circle that you’ll animate later.
This causes the text to both grow and move offscreen at the same time. The magic numbers probably seem a bit arbitrary, but they came from playing around and seeing what felt best. Feel free to tweak them yourself and see if you can come up with something you like better.
Add the following to the bottom of animateTransition(transitionContext:)
:
animateOldTextOffscreen(fromView: snapshot)
You pass the snapshot
to your new method to animate it offscreen.
Build and run to see your masterpiece so far.
OK, so it’s still not all that impressive, but this is how complex animations are done, one small building block at a time.
Fixing the Background
One annoying thing that’s immediately noticeable is that since the entire view is animating away, you’re seeing a black background behind it.
This black background is the containerView
and what you really want is for it to look like just the text is animating away, not the entire background. To fix this, you’ll need to add a new background view that doesn’t get animated.
In CircularTransition.swift, go to animateTransition(using:)
. After you grab a reference to the containerView
and before you add the snapshotView
as a subview, add the following code:
let backgroundView = UIView()
backgroundView.frame = toVC.mainView.frame
backgroundView.backgroundColor = fromVC.mainView.backgroundColor
Here you’re creating the backgroundView
, setting its frame to be full screen and its background color to match that of the backgroundView
.
Then, add your new background as a subview of the containerView
.
containerView.addSubview(backgroundView)
Build and run to see your improved animation.
Much better.
The Circular Mask Animation
Now that you’ve got the first chunk done, the next thing you need to do is the actual circular transition where the new view controller animates in from the button’s position.
Start by adding the following method to CircularTransition
:
func animate(toView: UIView, fromTriggerButton triggerButton: UIButton) {
}
This will complete the circular transition – you’ll implement it shortly!
In animateTransition(using:)
, add the following after animateOldTextOffscreen(fromView:snapshot)
:
containerView.addSubview(toVC.mainView)
animate(toView: toVC.mainView, fromTriggerButton: fromVC.triggerButton)
This adds your final view to the containerView
and will animate it – once you’ve implemented the animation!
Now you have the skeleton for the circular transition. However, the real keys to making this animation work are understanding the handy CAShapeLayer
class along with the concept of layer masking.