Blog

iOS Transition Animations: The proper way to do it

Date

13th January 2020

Read

17 min

Creator

Sam Miller

A Quick Introduction

Did you know (I only realised fairly recently!) that Apple‚ from iOS 7 onward‚ have provided UIKit APIs to apply custom animations to transitions in a pretty neat way. They allow us to define animation classes that can be applied to both push/pop transitions on a navigation stack‚ as well as presenting animations on modal popup transitions. 

The good news is that these are highly reusable too‚ as the way in which they are defined (by implementing a series of methods within a class which conforms to an animation protocol) allows us to easily integrate them into other parts of an app or even an entirely different app. 

Before we look at how these animation classes are implemented‚ we need to understand how we can include them in our apps. 

Note: Updated June 2021. There was a supplementary Xcode project that came with this blog. Most of the key parts are explained in this document however the Xcode project cannot be accessed through this web page. Please get in touch if you would like further information on the project.

Navigation Stack Animations and Modal Animations

As mentioned above‚ our custom animations can be applied to both modal styled transitions and navigation stack animations. The way in which we apply the animations to these mechanisms is quite different so we’ll need to focus on them separately.

We’ll start with the Navigation Stack mechanism first‚ as this one is slightly more straightforward.

Navigation Stack Animations

In the CustomAnimation Demo App attached to this tutorial‚ locate the PresentingViewController.swift file. This is the ViewController for our screen which performs the segues that will trigger our custom animations.

The first step is to make the PresentingViewController class conform to the UINavigationControllerDelegate.

class PresentingViewController: UIViewController‚ UINavigationControllerDelegate‚ UIViewControllerTransitioningDelegate {

This is because‚ in order for our custom animations to be considered by the system‚ we need to implement a method belonging to the UINavigationControllerDelegate protocol. The purpose of this method is to create an animation object (which we will come to in a little while) and return the object back to the system. Before we take a look at this method‚ we mustn’t forget to assign a delegate object to the navigation controller’s delegate property. We do this in the viewDidLoad method‚ and the delegate is our PresentingViewController.

self.navigationController?.delegate = self

Now we can implement the necessary method to provide our animation object. Before that though‚ a quick‚ simplified look at what is going on under the hood when we transition from one view controller to another.

At some point when the transition occurs‚ our view controller’s navigation controller checks to see if the source view controller (in this case‚ our PresentingViewController) conforms to the UINavigationControllerDelegate protocol. If it does‚ it will invoke the delegate method which we will implement in a moment (the one that returns our animations object)‚ otherwise the system will perform the default animations.

So let’s implement our UINavigationControllerDelegate delegate method.

func navigationController(_ navigationController: UINavigationController‚ animationControllerFor operation: UINavigationControllerOperation‚ from fromVC: UIViewController‚ to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.originFrame = self.pushButton.frame transition.forward = (operation == .push) return transition}

There is quite a bit going on here so let’s first look at the parameters that are being passed in to the method. The navigationController object is simply the navigation controller our view controller belongs to‚ and is the owner of the our UINavigationControllerDelegate object. The operation object is an enum instance which tells us whether we are pushing or popping on the navigation stack – very useful for when we build our animation. Finally‚ the fromVC and toVC are the source and destination view controllers that make up our transition.

You’ll notice that the delegate method return type‚ the animation object‚ is optional. If you return nil‚ the navigation controller will use the default system animations. This is useful if you want to conditionally determine which animations to use depending on the transition’s segue.

Now that we understand how to tell the system to use a custom animation‚ we can look at how we build the animations themselves. 

In our method implementation there is an instance of RevealFromFrameAnimator created. This is our custom object that conforms to the UIViewControllerAnimatedTransitioning delegate protocol. This is the object that the system will use to build and execute our custom animation. Notice that there are a couple of properties set on the object. These are properties we have implemented on our animation class to help build up the various aspects of the animation. To understand these‚ we will need to take a look at the RevealFromFrameAnimator class.

The first thing to notice is that our RevealFromFrameAnimator class implements the UIViewControllerAnimatedTransitioning protocol like so:

class RevealFromFrameAnimator: NSObject‚ UIViewControllerAnimatedTransitioning‚ CAAnimationDelegate {

Also note that the class inherits from NSObject. The class in which we implement our animation methods can be of any type. However‚ if you plan to make your animation classes interchangeable between projects‚ it does make sense to implement them in a class which has a sole purpose of providing only transition animations.

As a result of our class confirming to the UIViewControllerAnimatedTransitioning protocol‚ we need to implement a few methods. 

The first of these is the transitionDuration method. The purpose of this method is to inform the system the total duration of the transition in seconds. The system uses this value to synchronise any other system animations that occur alongside your own. For example‚ the navigation bar animation that occurs during a transition will also use this value. There is a single parameter (transitionContext) on this method that doesn’t get used here. It will be discussed in the next method as it is used there. 

The second and final method of note‚ is the animateTransition method. This is the method where we define our animations and how they affect the various views involved. The parameter mentioned above‚ the transitionContext‚ essentially contains all of the information associated with the transition. The three crucial items we obtain from this object are the containerView‚ the from View and the to View.

The containerView is the parent view of all views that are involved in the transition including the from view and the to view. The system creates this view for you and it will also add our from view to the container view (though do note that it only does this on the push and not the pop). It is then our job to add the to view to the container view. At this point‚ our transition view hierarchy is all set up and we are ready to animate.

You’ll notice that in the method‚ we are doing some slightly different variable assignments depending on the instance variable forward. This is a custom property on our animation class whose value is set from outside the class by our presenting view controller. It tells our animation class internally whether or not we are pushing or popping. This is also important because we need to know whether or not we are adding the presenting view ourselves‚ or letting the system add it as mentioned above.

[object Object]

The assignment of the forward property is done in the PresentingViewController in the UIViewControllerAnimatedTransitioning delegate method. The value itself is derived from whether or not the operation property‚ which we discussed above‚ is a push or a pop. We could have defined a UINavigationControllerOperation property on the animation class but as mentioned earlier‚ we want to make our animation class as easy as possible to reuse regardless of whether or not the transition is a navigation stack transition or a modal transition – hence naming it something a little more generalised.

Now it’s time for the animation code. For this particular animation‚ we are going to create the illusion that our destination view is revealed by expanding an arbitrary rectangle on the source view. That is to say‚ when we tap on a button displayed on the presenting view‚ the button will expand and reveal the destination view from below the button/source view.

To make this animation class as reusable as possible‚ we need a way to specify the rectangle from which our destination view emerges. In our presenting view controller‚ we simply set the animation class’ property originFrame to match the frame of the button we want to ‘reveal’ the destination view through. This is done at the point we instantiate our animation class in the UIViewControllerAnimatedTransitioning delegate method along with our forward property.

The next step is to establish our views based on the direction of the animation and add them to the containerView. Remember‚ if the animation direction is pushing/going forward‚ iOS will automatically add the origin view for us. We just need to add the animated view which‚ when going forward‚ is our destination view.

The way we create the illusion of our new view being ‘revealed’ is through applying a mask to the destination view’s layer mask. The mask itself is created and returned from the maskLayerForAnimation convenience method. The method takes a frame which is used to establish the position and size of the mask.

The frame for the mask depends on the direction of our animation. When going forward‚ the startFrame is the originFrame provided by UIViewControllerAnimatedTransitioning delegate method. When going backwards‚ the startFrame is the frame of the pushed view’s frame.

The newPath property is the path we want our mask layer to animate to‚ so going forward the path will be derived from the frame of the pushed view‚ and going backwards it will be the frame of our originFrame instance property. Essentially‚ these are just reversed depending on the direction. See below:

Note: To establish the most accurate frame for the pushed view‚ we use the finalFrame method and pass the appropriate view controller. This is important because Apple will perform the necessary calculations to return the correct frame in the event that your to and from view controllers have different navigation controller configurations. This can lead to undesired spacing and gaps at the top and bottom of your view if the frames are not calculated correctly.

var startFrame: CGRect! var newPath: CGPath! if self.forward { let destinationViewController = transitionContext.viewController(forKey: .to)! animatedView = destinationViewController.view originView = transitionContext.view(forKey: UITransitionContextViewKey.from)! containerView.addSubview(animatedView) startFrame = self.originFrame newPath = CGPath(rect: transitionContext.finalFrame(for: destinationViewController)‚ transform: nil) } else { animatedView = transitionContext.view(forKey: UITransitionContextViewKey.from)! originView = transitionContext.view(forKey: UITransitionContextViewKey.to)! containerView.addSubview(originView) containerView.addSubview(animatedView) startFrame = animatedView.frame newPath = CGPath(rect: self.originFrame‚ transform: nil) } let maskLayer = self.maskLayerForAnimation(frame: startFrame) animatedView.layer.mask = maskLayer

We now have enough information to create the animation‚ which is itself a CABasicAnimation object that is initialised with the keyPath property set to path‚ as this is the animatable property.

We then assign the CABasicAnimation object’s delegate property to our own animation object as we need to be informed of when the animation has finished.

Next‚ assign a ‘from’ and ‘to’ value on the animation. Remember‚ we are animating the mask’s path property on the view’s layer‚ so we will be providing a fromValue as a path and the toValue as a path. These values are based on the path property currently set on the mask‚ and the newPath value.

The duration property happens to be the same as the overall duration specified in the transitionDuration method. That said‚ it’s probably worth mentioning that this isn’t always the case‚ as your overall transition may contain more than just one animation. However‚ the sum of all of the animation’s durations should equal to the total duration specified in the transitionDuration. This is to ensure that the entire transition runs in sync with any other system animations that accompany your own.

The timingFunction property assignment simply gives our transitions a little polish with some easing.

Before we add the animation object to the layer‚ there is one more thing we need to do:

maskLayer.path = newPath

Modal Animations

We will break down the explanation into two segments‚ similar to how we walked through the navigation stack animations. Firstly‚ we’ll look at the code which informs the system that we want to perform custom animations. After this‚ we will look at the animation object itself.

So‚ let’s take another look at a line we saw in our first example:class PresentingViewController: UIViewController‚ UINavigationControllerDelegate‚ UIViewControllerTransitioningDelegate {

The delegate protocol to take note of this time around is the UIViewControllerTransitioningDelegate protocolIt is this protocol that defines which methods we need to implement in order to inform the system that we would like to customise our transitions that occur during presentation transitions. From the introduction‚ you’ll recall that the aim of this custom animation was to display the presented view from top-to-bottom as opposed to the default system animation bottom-to-top.

One of the key differences between the way we set this up in comparison to the previous example is the delegate property‚ which we assign as our presenting view controller. The delegate property we need to assign belongs to the view controller we are presenting and not the originating navigation controller as in the first example. Take a look at the following code:override func prepare(for segue: UIStoryboardSegue‚ sender: Any?) { if segue.identifier == "PresentedViewControllerSegue" { let presentedViewController = segue.destination as! UINavigationController presentedViewController.transitioningDelegate = self }}We need to assign our presenting view controller as the transitioningDelegate property on the presentedViewController. We retrieve this via the prepare method‚ which is invoked when we perform the segue set up in our storyboard file. At the point of presenting the new view controller‚ UIKit checks the transitioningDelegate to see if it implements the methods required to provide the system with the custom animations. This is done via two delegate methods:

func animationController(forPresented presented: UIViewController‚ presenting: UIViewController‚ source: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = PresentReverseAnimator() animator.presenting = true return animator}func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let animator = PresentReverseAnimator() animator.presenting = false return animator}

Unlike the navigation stack animations‚ we implement methods which are called based on the direction of our animation. If we are presenting our view controller‚ the forPresented method is called. Conversely‚ if we are dismissing the view controller‚ the forDismissed method is called. You’ll remember from the navigation stack methods that we used one method belonging to the UINavigationControllerDelegate protocol‚ where the operation attribute would inform our method implementation of the animation direction. In this example‚ we have a property defined on our animation class‚ similar to the navigation stack animation’s forward property‚ called isPresenting. The value of this property is derived from the method in which our animation object is created. So if the forPresented is called‚ the isPresenting is set to true. If the forDismissed method is called‚ it is set to false.

Looking at the code in each method‚ we find similarity in the implementation of the UINavigationControllerDelegate method from the first example. We create an instance of our custom animation object; provide the direction via the isPresenting property‚ and finally return the instance. The results of doing this are identical to the navigation stack animations. If we return an object that conforms to the UIViewControllerAnimatedTransitioning protocol‚ UIKit will use the animation defined in the animation class for the transition. If we return nil‚ the system will the use the default animations provided by UIKit.

[object Object]

What about the animation itself? Let’s take a quick look at the code for our PresentReverseAnimator animation class.

You’ll notice a lot of similarities to the first example. We have defined our class as an NSObject whose sole purpose is to provide UIKit with an animation‚ thus allowing us to easily reuse our animation in other apps. It conforms to the UIViewControllerAnimatedTransitioning protocol where we implement the two principle methods that will provide the system with the animation.

The first of these is the transitionDuration method‚ where we return the total duration of the transition animation. Like the first example we keep the value the same as this is roughly the duration of the system animations throughout iOS.

The second of the methods is the animateTransition method. The big difference in this implementation is that‚ as a result of the animation being a lot simpler than the reveal animation‚ we are not using Core Animation but the static UIView method animate.

Similar to the reveal animation‚ we retrieve the context object from the animateTransition attribute transitionContext. This allows us to retrieve our ‘to’ and ‘from’ views so that we may animate them appropriately.

Depending on the direction of the animation (derived from our isPresenting property)‚ we assign initial frames to the to and from view‚ build a destination frame for the view that will animate (again‚ using the finalFrame method discussed above)‚ and then update the to view’s frame within the animation block. Remember‚ on the initial presentation animation we don’t need to add the from view to the container view as UIKit takes care of this for us.

If we are presenting‚ the only thing we need to do is change the initial y position of the animatedView’s frame so that it starts off the screen from the top. We can do this by assigning negative the height of the view. The destinationFrame’s origin y value is simply the result of the finalFrame call after passing in the destination view controller.

If we are dismissing‚ we don’t even need to set any initial frames on the views as they are already where we want them to be. All we need to do is build the destination frame‚ which is simply the source frame from the presentation animation where the presenting screen is off-screen from the top.

Now that we have everything set up for both animation directions‚ we can assign the destination frame to the animatedView in the animation block.

On completion of the animation in the completion block‚ we call the completeTransition to inform UIKit that our transition is finished.

Summary:

And that’s it. A very simple way to reverse the system animation for presenting and dismissing view controllers through the navigation controller’s present method. That also ends the tutorial. Below are a few notes summarising everything:

  • It’s very easy to inform UIKit that you want your app to perform custom animations during a transition through the

     

    UINavigationControllerDelegate

     

    protocol and the

     

    UIViewControllerTransitioningDelegate

     

    protocol.

  • Animator classes can be reused easily across applications. It’s best to write them in a way that is not tied to a specific project.

  • There are lots of other cool things you can do with custom transitions including interactive animators. See the Apple Documentation link below for more information.

References:

  1. Apple Developer Library – View Controller Programming Guide for iOS: Customizing the Transition Animations.