How To Make A View Controller Transition Animation Like in the Ping App

Rounak Jain

Ping

Update 23/4/2015: Updated for Xcode 6.3 / Swift 1.2

The makers of the anonymous social networking app Secret recently released a new app called Ping, which allows users to receive notifications about topics they’re interested in.

One thing that stood out about Ping is its nice, circular animation between the main screen and the menu, as you can see in the animation to the right.

Every time I see a really neat animation, I do what everybody does: I think “Now how would I implement that on iOS…” — wait, normal people don’t think that?! :]

In this tutorial, you’ll learn how to implement this cool animation in Swift. In the process, you’ll learn about using shape layers, masking, the UIViewControllerAnimatedTransitioning protocol, the UIPercentDrivenInteractiveTransition class, and more.

Note this tutorial assumes you know the basics of iOS development and Swift. If you are a beginner, check out some of the many other tutorials on our site.

Overall Strategy

In Ping, the animation happens when you transition from one view controller to another.

In iOS, you can make custom animations between view controllers by putting both view controllers inside a UINavigationController, and implement iOS 7’s UIViewControllerAnimatedTransitioning protocol to animate the transition.

You’ll get into the details below, but essentially this protocol allows you to:

  • Specify the duration of the animation
  • Create a container view with references two both view controllers
  • Have the freedom to write any animation you can imagine!

You do all this with higher-level UIView animations or lower-level Core Animation animations (in this tutorial, you’ll do the latter).

Implementation Strategy

Now that you know where the coding action happens, the next bit of discussion revolves around how to actually implement the circle transition.

If I were to try to describe the animation in words it would be some thing like:

  • There’s a circle that originates from the button on the top right; it acts as a viewport into the view that appears.
  • In other words, the circle acts as a mask that shows everything inside its bounds, and hides everything that’s outside.

You can achieve this effect by using mask on CALayer, and also its alpha channel that decides which portions of the layer to show.

An alpha value of 1 shows the layer content underneath, while an alpha value of 0 hides content beneath. Anything in-between partially reveals the layer’s content. Here’s a diagram to explain this:

mask diagram

Now that you know about mask, the next step is to decide what kind of mask to apply. Since the animation has a circular mask, the natural choice is CAShapeLayer. To animate the circle, you simply increase the radius of the circle mask.

Getting Started

Note: This section is optional for those who want to build the project from scratch. If you’re an advanced iOS developer, feel free to skip to the Custom Animation section, where you’ll find a starter project and can dive right into the animations.

Now that you’ve sorted out the strategy, it’s time to write some code!

Create a new project in Xcode by clicking select File\New\Project, and choose iOS\Application\Single View Application.

step 1

Name the project CircleTransition, select Swift for Language, and select iPhone for Devices.

step 2

Start by opening Main.storyboard. You’ll see a single view controller, but the transition needs multiple view controllers to switch between.

But first, you should embed the view controller in a navigation controller. To do this, select the view controller, either in the Document Outline or on the canvas, and select Editor\Embed In\Navigation Controller.

Next select the navigation controller, and in the Utilities panel on the right open the Attributes Inspector (4th tab) and uncheck the Shows Navigation Bar checkbox, because the design doesn’t have a navigation bar.

Screenshot 2014-10-25 03.25.11

Now add another view controller to the storyboard. Line up both view controllers horizontally with the navigation controller for clarity.

ViewControllers

Select the new view controller, and in the Utilities panel on the right, select the Identity Inspector (on the third tab). Change the class type to ViewController, so it corresponds with the ViewController class file Xcode created for you.

Screenshot 2014-10-23 03.20.25

Next, add a button to the top right of each view controller. For both buttons, double-click and press backspace to set the button title to an empty string.

Screenshot 2014-10-23 12.16.11

Also set each button’s background color to black.

Now it’s time to position the buttons with Auto Layout. Select the button on the first view controller, then select the Pin button in the lower-right corner, and configure it as follows:

  • Click the light red brackets for the top and right, and set them each to 10
  • Set the Width and Height to 44
  • Set Update Frames to Items of New Constraints

PinConstraints2

Click Add 4 Constraints, and the button will resize. Repeat this for the other button as well.

The last thing to configure is the shape of the buttons — you need to make them circular by setting a corner radius. Rather than painstakingly coding it, switch to the Identity Inspector and use the User Defined Runtime Attributes to define the key path and set the cornerRadius for the button’s layer.

Screenshot 2014-10-23 12.54.26

Interface Builder will not reflect the circular shape of the buttons, but build and run your app and you’ll see a black circle in the top right corner.

iOS Simulator Screen Shot Nov 3, 2014, 10.27.11 PM

Now you need some content in those view controllers. Why not start giving each a background color?

For the first view controller, set it to have a green background color, and for the second view controller, set it to have a yellow background.

Screenshot 2014-10-23 13.42.48

Now add image views to both view controllers. Pin both their width and height to 300 points and center them in their superviews by selecting Align in the lower-right corner and selecting Horizontal Center in Container and Vertical Center in Container.

Screen Shot 2014-11-03 at 10.33.41 PM

Adjust their frames to the correct values by selecting the buttons, clicking Resolve Auto Layout Issues, then Update Frames, and your canvas should look like this:

Screenshot 2014-10-25 14.36.26

Download these images of iPhones and iPads, add them to your project and then assign them as the image for each image view. Ensure that the content mode of both image views is Aspect Fit.

Screen Shot 2014-11-03 at 10.37.33 PM

Your canvas should now look similar to this:
2014-10-25_15-40-43

Wire It Up

Congrats! You have the skeleton of an app. Now, it’s time to give it some digital “flesh” by wiring up the view controllers and buttons to their actions.

Right click the button on the first view controller, and then drag the action outlet to the second view controller.

Screenshot 2014-10-23 14.33.38

You’ll see a popup menu appear: select show.

Screenshot 2014-10-23 14.33.40
Doing this will push the second view controller when the button is tapped.

Later in this tutorial you’ll need a reference to this segue, so click on it, and open the Attributes Inspector on the left. Name the segue identifier PushSegue.

Screen Shot 2014-11-04 at 11.13.41 PM

Build and run the app to ensure that the second view controller is being pushed.

Now you’ll wire the button on the second view controller. You want to “pop” the view controller, so you’ll need to write a simple instance method in the ViewController class:

@IBAction func circleTapped(sender:UIButton) {
  self.navigationController?.popViewControllerAnimated(true)
}

While you’re in the ViewController class, add a weak property to hold a reference to the button.

@IBOutlet weak var button: UIButton!

Go back to Main.storyboard and do the following for both view controllers:

  • Right-click on the button
  • Drag the Touch Up Inside circle to the View Controller item at the top of the scene
  • Select circleTapped()

Screenshot 2014-10-23 15.04.58

Right-click each button once more, and this time, drag a referencing outlet to each view controller and connect it to the button property.

Screenshot 2014-10-23 21.01.32

Screenshot 2014-10-23 21.01.35

Build and run again, and you’ll see that you now have fully functional push and pop.

normal push


Custom Animation

If you skipped the previous sections, then download this starter project so you can jump in with fully configured view controllers and buttons.

To write a custom push or pop animation, you need to implement the UINavigationControllerDelegate protocol’s animationControllerForOperation method.

Create a new file with the iOS\Source\Cocoa Touch Class template. Set the class name to NavigationControllerDelegate.

After creating the file, mark class as implementing UINavigationControllerDelegate as follows:

class NavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
 
}

Next open Main.storyboard to assign an instance of this class as the storyboard’s UINavigationController’s delegate.

To do this, search for Object in the library on the right, and drag and drop it under Navigation Controller Source on the left.
Screenshot 2014-10-23 15.39.30
Now click on the object, go to the Identity Inspector on the right, and change its class to NavigationControllerDelegate.
Screenshot 2014-10-23 15.40.25
Next, assign this object as the UINavigationController’s delegate by right-clicking the UINavigationController in the left panel, and dragging its delegate property to the NavigationControllerDelegate object:
Screenshot 2014-10-23 15.41.18

Head back to NavigationControllerDelegate and add this placeholder method:

func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
  return nil
}

Note that the body of the method empty; you’ll be working on that in a moment.

This method receives the two view controllers that the navigation controller is transitioning between, and its job is to return an object that implements UIViewControllerAnimatedTransitioning.

So you need to create one of those! To do this, create a new Cocoa Touch Class by navigating to File\New\File and name it CircleTransitionAnimator.

Screenshot 2014-10-23 15.52.17

Declare that you’re implementing the UIViewControllerAnimatedTransitioning protocol:

class CircleTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {

Next you need to add the required methods for those protocols.

Start by adding the first method as follows:

func transitionDuration(transitionContext: UIViewControllerContextTransitioning) -> NSTimeInterval {
    return 0.5
}

In this method, you need to return the duration of the animation. You want an animation that lasts 0.5 seconds, so you simply return 0.5:

Next, add this property to the class:

weak var transitionContext: UIViewControllerContextTransitioning?

You’ll need this in a moment to store the transition context.

Next, add the second required method from the protocol:

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
 
  //1
  self.transitionContext = transitionContext
 
  //2
  var containerView = transitionContext.containerView()
  var fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) as! ViewController
  var toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) as! ViewController
  var button = fromViewController.button
 
  //3
  containerView.addSubview(toViewController.view)
 
  //4
  var circleMaskPathInitial = UIBezierPath(ovalInRect: button.frame)
  var extremePoint = CGPoint(x: button.center.x - 0, y: button.center.y - CGRectGetHeight(toViewController.view.bounds))
  var radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
  var circleMaskPathFinal = UIBezierPath(ovalInRect: CGRectInset(button.frame, -radius, -radius))
 
  //5
  var maskLayer = CAShapeLayer()
  maskLayer.path = circleMaskPathFinal.CGPath
  toViewController.view.layer.mask = maskLayer
 
  //6
  var maskLayerAnimation = CABasicAnimation(keyPath: "path")
  maskLayerAnimation.fromValue = circleMaskPathInitial.CGPath
  maskLayerAnimation.toValue = circleMaskPathFinal.CGPath
  maskLayerAnimation.duration = self.transitionDuration(transitionContext)
  maskLayerAnimation.delegate = self
  maskLayer.addAnimation(maskLayerAnimation, forKey: "path")
}

Let’s go over this step by step:

  1. Lets you keep a reference to the transitionContext out of the scope of this method, so you can access it later.
  2. Gets references to the container view, the from and to view controller, as well as the button from where the action was triggered. The container view is the view in which the animation happens; the from and to view controllers are the same view controllers that take part in the animation.
  3. Adds the toViewController as a subview to containerView.
  4. Creates two circular UIBezierPath instances; one is the size of the button, and the second has a radius large enough to cover the entire screen. The final animation will be between these two bezier paths.
  5. Creates a new CAShapeLayer to represent the circle mask. You assign its path value with the final circular path after the animation to avoid the layer snapping back after the animation completes.
  6. Creates a CABasicAnimation on the path key path that goes from circleMaskPathInitial to circleMaskPathFinal. You also register a delegate, as you’ll do some cleanup after the animation completes.

Next, implement animationDidStop() in the same class to do a little cleanup:

override func animationDidStop(anim: CAAnimation!, finished flag: Bool) {
  self.transitionContext?.completeTransition(!self.transitionContext!.transitionWasCancelled())
  self.transitionContext?.viewControllerForKey(UITransitionContextFromViewControllerKey)?.view.layer.mask = nil
}

The first line is a way to tell iOS the transition finished. Because the animation is complete, you can remove the mask.

The final step is to actually use CircleTransitionAnimator.

Go back to NavigationControllerDelegate.swift and modify the stub method you added earlier as follows:

func navigationController(navigationController: UINavigationController,
 animationControllerForOperation operation: UINavigationControllerOperation,
 fromViewController fromVC: UIViewController,
 toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return CircleTransitionAnimator()
}

This is a simple modification to return a new instance of the CircleTransitionAnimator you just created.

Build and run the app, and you should now see the animation in full glory!

circle

Congrats, you have replicated the animation from the Ping app! If that’s all you wanted, you can stop reading here; but if you want to learn how to make the animation interactive as well, keep reading!

An Interactive Gesture Animation

Once you have the animation up and running, you can turn your attention to another feature of custom view controller transitions: an interactive “back” gesture. Since tapping is passé, make sure you implement this to add depth to the UI.

The interactive gesture starts off with a call to navigationController:interactionControllerForAnimationController:. This is a UINavigationControllerDelegate method that expects you to return an object that conforms to UIViewControllerInteractiveTransitioning.

The iOS SDK provides you a class called UIPercentDrivenInteractiveTransition that already implements this protocol, and it does a lot of the interactive gesture handling for you.

To use this, open NavigationControllerDelegate.swift and add this property and new method:

var interactionController: UIPercentDrivenInteractiveTransition?
 
func navigationController(navigationController: UINavigationController, 
interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
  return self.interactionController
}

Now go back to thinking about this swipe back gesture. Obviously, you need a gesture recognizer.

I Knew That!

You’ll be adding the gesture recognizer to the navigation controller, within it’s navigation controller delegate. To do this, you’ll need a reference to the navigation controller, so open NavigationControllerDelegate.swift and add this property:

@IBOutlet weak var navigationController: UINavigationController?

Open Main.storyboard, and connect this property to the navigation controller by right-clicking on the Navigation Controller Delegate object on the left, and then dragging from the navigationController property to the navigation controller in the storyboard.
Screenshot 2014-10-24 02.18.12

Now hop back to NavigationControllerDelegate.swift and implement awakeFromNib() as follows:

override func awakeFromNib() {
  super.awakeFromNib()
  var panGesture = UIPanGestureRecognizer(target: self, action: Selector("panned:"))
  self.navigationController!.view.addGestureRecognizer(panGesture)
}

This creates UIPanGestureRecognizer, and adds this object to the navigation controller’s view. You’ll get gesture callbacks in the panned: method inside the same class.

Next, implement the method as follows:

//1
@IBAction func panned(gestureRecognizer: UIPanGestureRecognizer) {
  switch gestureRecognizer.state {
  case .Began:
    self.interactionController = UIPercentDrivenInteractiveTransition()
    if self.navigationController?.viewControllers.count > 1 {
      self.navigationController?.popViewControllerAnimated(true)
    } else {
      self.navigationController?.topViewController.performSegueWithIdentifier("PushSegue", sender: nil)
    }
 
    //2
  case .Changed:
    var translation = gestureRecognizer.translationInView(self.navigationController!.view)
    var completionProgress = translation.x/CGRectGetWidth(self.navigationController!.view.bounds)
    self.interactionController?.updateInteractiveTransition(completionProgress)
 
    //3
  case .Ended:
    if (gestureRecognizer.velocityInView(self.navigationController!.view).x > 0) {
      self.interactionController?.finishInteractiveTransition()
    } else {
      self.interactionController?.cancelInteractiveTransition()
    }
    self.interactionController = nil
 
    //4
  default:
    self.interactionController?.cancelInteractiveTransition()
    self.interactionController = nil
  }
}

Let’s break this code block down, case by case.

  1. .Began: As soon as the gesture gets recognised, it initializes a UIPercentDrivenInteractiveTransition object and assigns it to the interactionController property.
    • If you’re on the first view controller, it initiates a push; and if the second then it initiates a pop. Popping is fairly easy, but to push you’ll have to manually perform the segue from the button you created earlier.
    • In turn, the push or pop call triggers the NavigationControllerDelegate method call that returns self.interactionController. So by then, you have a non-nil value in the property.
  2. .Changed: In this state, you complete the progress of the gesture and update the interactionController with the progress. It does the tough job of interpolating the animation, and with this part, Apple does all the hard work, so you don’t have to do anything! :]
  3. .Ended: Here, you see the velocity of the pan gesture. If it’s positive, the transition completes, and if not, it’s cancelled. You also set the interactionController to nil so it acts as the cleanup crew.
  4. default: If any other state, you simply cancel the transition and set the interactionController to nil.

Build and run the app, then swipe from left to right — you should see the same animation, but this time its under the control of your finger.

circle interactive

Where To Go From Here?

Here is the completed project from the above tutorial.

I hope you enjoyed this tutorial about how to make this simple, but cool animation that’s much like what you see in Ping. You can implement similar animations in your apps, and can change the look and feel by modifying the button, background colors and animation speed.

You learned quite a few things in here, including using shape layers, masking, UIViewControllerAnimatedTransitioning protocol, the UIPercentDrivenInteractiveTransition class, and more.

If you have questions, comments or would like to show off how you took the concepts in this lesson to the next level, please join the discussion below!

Rounak Jain

I'm an iOS developer who playing with animations. I work at Tinder. My free time is spent on a Dribbble app called Design Shots and iosdevtips.co.

Find me on email, Twitter or LinkedIn.

Other Items of Interest

raywenderlich.com Weekly

Sign up to receive the latest tutorials from raywenderlich.com each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 8 total!

Swift Team

... 16 total!

iOS Team

... 34 total!

Android Team

... 15 total!

macOS Team

... 13 total!

Apple Game Frameworks Team

... 14 total!

Unity Team

... 11 total!

Articles Team

... 10 total!

Resident Authors Team

... 6 total!