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.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

  1. The triggerButton will be the button the user tapped.
  2. The contentTextView will be the text view to animate on or offscreen.
  3. 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:

  1. Removed the fromVC‘s view from the container.
  2. 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:

  1. You define an animation that will take 0.25 seconds to complete and eases into its animation curve.
  2. You animate the view’s center down and to the left offscreen.
  3. 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.

Note: There is still one warning in CircularTransition.swift. Don’t worry; you will fix it soon!

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.