Development

iOS Transition Animations: The proper way to do it.

Nov 17, 2017
code used in blogpost overlaid on background of hands holding iphone

 

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: There is a supplementary Xcode project that comes with this blog. Most of the key parts are explained in this document, but feel free to have a play around with the project too. Sometimes it helps.

 

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.

 

push_view.gif

 

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:

 


var startFrame: CGRect!
var newPath: CGPath!

if self.forward {
    startFrame = self.originFrame
    newPath = CGPath(rect: animatedView.frame, transform: nil)
} else {
    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
 

 

We need to ensure that we assign the newPath to the mask layer’s path property because the changes that will be applied via the animation will not be applied permanently. That is to say, if we perform the forward animation and do not include the above line, the animation will work but the view’s appearance will reset back to its original state before the animation occurred.

 

The final line in this method, simply adding the animation object to the mask layer, will run the animation. Just make sure to specify the correct keyPath again. In this case, the path property.

 

As mentioned above, our Animation class conforms to the CAAnimationDelegate delegate protocol and we assign our animation class as the delegate. The reason for this is that we need to explicitly call the completeTransition method on the UIViewControllerContextTransitioning object. In order to do this we need to wait for our CABasicAnimation to finish, which we do by implementing the animationDidStop delegate method. In here we call the completeTransition method on our context object.

 

Note that we have assigned this to an instance variable so that we may retain scope outside the animateTransition method from where it originated. The documentation on the completeTransition method states that it ‘…effectively updates internal view controller state at the end of the transition.’ In other words, if you try to run your app without this line, you will not be able to interact with the app once the transition completes as the destination view controller will not be in the correct state.

 

And that’s it. You now know how to apply a custom animation to a transition via pushing and popping, to and from a navigation stack.

 

We’ll now take a look at how to customise the animations when using modal animations during the presentation of and dismissing of view controllers.

 

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 protocol. It 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.

 

presenting_view.gif

 

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 and build a destination frame for the view that will animate 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 will be set to 0 as this is the final position of the view once the animation has completed.

 

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.

 

Looking for more App Development tips?

the_importance_of_django_model_managers_macbook.jpg

Discover the benefits of using model managers in development

 

Share this article
Sam Miller

Author Sam Miller

Sam has been developing iOS apps for over 5 years for big clients including Random House, PBS and Sesame Street. He has a degree in Internet Computing from Northumbria University. His interests include gaming, reading horror novels and playing guitar.

View more posts by this author

FREE WHITEPAPER

Challenges and opportunities in ARToolKit development.

The explosive rise of augmented reality presents a number of opportunities and challenges for developers. Learn more about the technicalities of AR and AR applications, as well as:

•  AR development software such as ARKit, ARCore, Daqri-ARToolkit and more.
•  The industries AR applications will transform.
•  Considerations in building an AR Toolkit.
•  The future of AR development.
Get your free whitepaper here