{"id":5217,"date":"2020-01-13T00:00:00","date_gmt":"2020-01-13T00:00:00","guid":{"rendered":"https:\/\/hedgehoglab.com\/ios-transition-animations-the-proper-way-to-do-it\/"},"modified":"2023-11-06T09:54:34","modified_gmt":"2023-11-06T08:54:34","slug":"ios-transition-animations-the-proper-way-to-do-it","status":"publish","type":"post","link":"https:\/\/hedgehoglab.com\/ios-transition-animations-the-proper-way-to-do-it\/","title":{"rendered":"iOS Transition Animations: The proper way to do it"},"content":{"rendered":"\n

A Quick Introduction<\/h3>\n\n\n\n

Did you know (I only realised fairly recently!) that Apple\u201a from iOS 7 onward\u201a 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\u201a as well as presenting animations on modal popup transitions. <\/p>\n\n\n\n

The good news is that these are highly reusable too\u201a 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. <\/p>\n\n\n\n

Before we look at how these animation classes are implemented\u201a we need to understand how we can include them in our apps. <\/p>\n\n\n\n

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. <\/i><\/p>\n\n\n\n

Navigation Stack Animations and Modal Animations<\/h3>\n\n\n\n

As mentioned above\u201a 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\u2019ll need to focus on them separately.<\/p>\n\n\n\n

We\u2019ll start with the Navigation Stack mechanism first\u201a as this one is slightly more straightforward.<\/p>\n\n\n\n

Navigation Stack Animations<\/h3>\n\n\n\n

In the CustomAnimation<\/i> Demo App attached to this tutorial\u201a locate the PresentingViewController.swift<\/i> file. This is the ViewController for our screen which performs the segues that will trigger our custom animations.<\/p>\n\n\n\n

The first step is to make the PresentingViewController <\/i>class conform to the UINavigationControllerDelegate.<\/i><\/p>\n\n\n\n

class PresentingViewController: UIViewController\u201a UINavigationControllerDelegate\u201a UIViewControllerTransitioningDelegate { <\/code><\/p>\n\n\n\n

This is because\u201a in order for our custom animations to be considered by the system\u201a we need to implement a method belonging to the UINavigationControllerDelegate <\/i>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\u201a we mustn\u2019t forget to assign a delegate object to the navigation controller\u2019s delegate property. We do this in the viewDidLoad <\/i>method\u201a and the delegate is our PresentingViewController.<\/i><\/p>\n\n\n\n

self.navigationController?.delegate = self <\/code><\/p>\n\n\n\n

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

At some point when the transition occurs\u201a our view controller\u2019s navigation controller checks to see if the source view controller (in this case\u201a our PresentingViewController<\/i>) conforms to the UINavigationControllerDelegate <\/i>protocol. If it does\u201a it will invoke the delegate method which we will implement in a moment (the one that returns our animations object)\u201a otherwise the system will perform the default animations.<\/p>\n\n\n\n

So let\u2019s implement our UINavigationControllerDelegate<\/i> delegate method.<\/p>\n\n\n\n

func navigationController(_ navigationController: UINavigationController\u201a animationControllerFor operation: UINavigationControllerOperation\u201a from fromVC: UIViewController\u201a to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { transition.originFrame = self.pushButton.frame transition.forward = (operation == .push) return transition}<\/code><\/p>\n\n\n\n

There is quite a bit going on here so let\u2019s first look at the parameters that are being passed in to the method. The navigationController<\/i> object is simply the navigation controller our view controller belongs to\u201a and is the owner of the our UINavigationControllerDelegate <\/i>object. The operation <\/i>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\u201a the fromVC<\/i> and toVC<\/i> are the source and destination view controllers that make up our transition.<\/p>\n\n\n\n

You\u2019ll notice that the delegate method return type\u201a the animation object\u201a is optional. If you return nil\u201a 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\u2019s segue.<\/p>\n\n\n\n

Now that we understand how to tell the system to use a custom animation\u201a we can look at how we build the animations themselves. <\/p>\n\n\n\n

In our method implementation there is an instance of RevealFromFrameAnimator <\/i>created. This is our custom object that conforms to the UIViewControllerAnimatedTransitioning <\/i>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\u201a we will need to take a look at the RevealFromFrameAnimator <\/i>class.<\/p>\n\n\n\n

The first thing to notice is that our RevealFromFrameAnimator <\/i>class implements the UIViewControllerAnimatedTransitioning <\/i>protocol like so:<\/p>\n\n\n\n

class RevealFromFrameAnimator: NSObject\u201a UIViewControllerAnimatedTransitioning\u201a CAAnimationDelegate { <\/code><\/p>\n\n\n\n

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

As a result of our class confirming to the UIViewControllerAnimatedTransitioning <\/i>protocol\u201a we need to implement a few methods. <\/p>\n\n\n\n

The first of these is the transitionDuration <\/i>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\u201a the navigation bar animation that occurs during a transition will also use this value. There is a single parameter (transitionContext<\/i>) on this method that doesn\u2019t get used here. It will be discussed in the next method as it is used there. <\/p>\n\n\n\n

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

The containerView<\/i> is the parent view of all views that are involved in the transition including the from<\/i> view and the to<\/i> view. The system creates this view for you and it will also add our from<\/i> 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<\/i> view to the container view. At this point\u201a our transition view hierarchy is all set up and we are ready to animate.<\/p>\n\n\n\n

You\u2019ll notice that in the method\u201a we are doing some slightly different variable assignments depending on the instance variable forward<\/i>. 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\u201a or letting the system add it as mentioned above.<\/p>\n\n\n\n

\"[object<\/figure>\n\n\n\n

The assignment of the forward<\/i> property is done in the PresentingViewController in the UIViewControllerAnimatedTransitioning <\/i>delegate method. The value itself is derived from whether or not the operation <\/i>property\u201a which we discussed above\u201a is a push or a pop. We could have defined a UINavigationControllerOperation <\/i>property on the animation class but as mentioned earlier\u201a 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.<\/p>\n\n\n\n

Now it\u2019s time for the animation code. For this particular animation\u201a 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\u201a when we tap on a button displayed on the presenting view\u201a the button will expand and reveal the destination view from below the button\/source view.<\/p>\n\n\n\n

To make this animation class as reusable as possible\u201a we need a way to specify the rectangle from which our destination view emerges. In our presenting view controller\u201a we simply set the animation class\u2019 property originFrame <\/i>to match the frame of the button we want to \u2018reveal\u2019 the destination view through. This is done at the point we instantiate our animation class in the UIViewControllerAnimatedTransitioning <\/i>delegate method along with our forward <\/i>property.<\/p>\n\n\n\n

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

The way we create the illusion of our new view being \u2018revealed\u2019 is through applying a mask to the destination view\u2019s layer mask. The mask itself is created and returned from the maskLayerForAnimation<\/i> convenience method. The method takes a frame which is used to establish the position and size of the mask.<\/p>\n\n\n\n

The frame for the mask depends on the direction of our animation. When going forward\u201a the startFrame<\/i> is the originFrame<\/i> provided by UIViewControllerAnimatedTransitioning <\/i>delegate method. When going backwards\u201a the startFrame<\/i> is the frame of the pushed view\u2019s frame.<\/p>\n\n\n\n

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

Note: To establish the most accurate frame for the pushed view\u201a 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.<\/i><\/p>\n\n\n\n

\tvar startFrame: CGRect!\tvar newPath: CGPath! if self.forward {\t\tlet destinationViewController = transitionContext.viewController(forKey: .to)!\t\tanimatedView = destinationViewController.view\t\toriginView = transitionContext.view(forKey: UITransitionContextViewKey.from)!\t\tcontainerView.addSubview(animatedView)\t\tstartFrame = self.originFrame\t\tnewPath = CGPath(rect: transitionContext.finalFrame(for: destinationViewController)\u201a transform: nil) } else {\t\tanimatedView = transitionContext.view(forKey: UITransitionContextViewKey.from)!\t\toriginView = transitionContext.view(forKey: UITransitionContextViewKey.to)!\t\tcontainerView.addSubview(originView)\t\tcontainerView.addSubview(animatedView)\t\tstartFrame = animatedView.frame\t\tnewPath = CGPath(rect: self.originFrame\u201a transform: nil) } let maskLayer = self.maskLayerForAnimation(frame: startFrame) animatedView.layer.mask = maskLayer<\/code><\/p>\n\n\n\n

We now have enough information to create the animation\u201a which is itself a CABasicAnimation<\/i> object that is initialised with the keyPath<\/i> property set to path\u201a <\/i>as this is the animatable property.<\/p>\n\n\n\n

We then assign the CABasicAnimation <\/i>object\u2019s delegate property to our own animation object as we need to be informed of when the animation has finished.<\/p>\n\n\n\n

Next\u201a assign a ‘from’ and ‘to’ value on the animation. Remember\u201a we are animating the mask\u2019s path property on the view\u2019s layer\u201a so we will be providing a fromValue<\/i> as a path and the toValue<\/i> as a path. These values are based on the path property currently set on the mask\u201a and the newPath<\/i> value.<\/p>\n\n\n\n

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

The timingFunction <\/i>property assignment simply gives our transitions a little polish with some easing.<\/p>\n\n\n\n

Before we add the animation object to the layer\u201a there is one more thing we need to do:<\/p>\n\n\n\n

maskLayer.path = newPath<\/code><\/p>\n\n\n\n

Modal Animations<\/b><\/h3>\n\n\n\n

We will break down the explanation into two segments\u201a similar to how we walked through the navigation stack animations. Firstly\u201a we\u2019ll look at the code which informs the system that we want to perform custom animations. After this\u201a we will look at the animation object itself.<\/p>\n\n\n\n

So\u201a let’s take another look at a line we saw in our first example:class PresentingViewController: UIViewController\u201a UINavigationControllerDelegate\u201a UIViewControllerTransitioningDelegate {<\/code><\/p>\n\n\n\n

The delegate protocol to take note of this time around is the UIViewControllerTransitioningDelegate <\/i>protocol. <\/i>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\u201a you\u2019ll 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.<\/p>\n\n\n\n

One of the key differences between the way we set this up in comparison to the previous example is the delegate property\u201a 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\u201a sender: Any?) { if segue.identifier == \"PresentedViewControllerSegue\" { let presentedViewController = segue.destination as! UINavigationController presentedViewController.transitioningDelegate = self }}<\/code>We need to assign our presenting view controller as the transitioningDelegate <\/i>property on the presentedViewController. <\/i>We retrieve this via the prepare <\/i>method\u201a which is invoked when we perform the segue set up in our storyboard file. At the point of presenting the new view controller\u201a UIKit checks the transitioningDelegate <\/i>to see if it implements the methods required to provide the system with the custom animations. This is done via two delegate methods:<\/p>\n\n\n\n

func animationController(forPresented presented: UIViewController\u201a presenting: UIViewController\u201a 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}<\/code><\/p>\n\n\n\n

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

Looking at the code in each method\u201a we find similarity in the implementation of the UINavigationControllerDelegate <\/i>method from the first example. We create an instance of our custom animation object; provide the direction via the isPresenting <\/i>property\u201a <\/i>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 <\/i>protocol\u201a UIKit will use the animation defined in the animation class for the transition. If we return nil\u201a the system will the use the default animations provided by UIKit.<\/p>\n\n\n\n

\"[object<\/figure>\n\n\n\n

What about the animation itself? Let\u2019s take a quick look at the code for our PresentReverseAnimator <\/i>animation class.<\/p>\n\n\n\n

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

The first of these is the transitionDuration <\/i>method\u201a 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.<\/p>\n\n\n\n

The second of the methods is the animateTransition <\/i>method. The big difference in this implementation is that\u201a as a result of the animation being a lot simpler than the reveal animation\u201a we are not using Core Animation but the static UIView<\/i> method animate.<\/i><\/p>\n\n\n\n

Similar to the reveal animation\u201a we retrieve the context object from the animateTransition <\/i>attribute transitionContext. <\/i>This allows us to retrieve our ‘to’ and ‘from’ views so that we may animate them appropriately.<\/p>\n\n\n\n

Depending on the direction of the animation (derived from our isPresenting property)\u201a we assign initial frames to the to and from view\u201a build a destination frame for the view that will animate (again\u201a using the finalFrame method discussed above)\u201a and then update the to view’s frame within the animation block. Remember\u201a 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.<\/p>\n\n\n\n

If we are presenting\u201a 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.<\/p>\n\n\n\n

If we are dismissing\u201a we don\u2019t 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\u201a which is simply the source frame from the presentation animation where the presenting screen is off-screen from the top.<\/p>\n\n\n\n

Now that we have everything set up for both animation directions\u201a we can assign the destination frame to the animatedView <\/i>in the animation block.<\/p>\n\n\n\n

On completion of the animation in the completion block\u201a we call the completeTransition <\/i>to inform UIKit that our transition is finished.<\/p>\n\n\n\n

Summary:<\/b><\/h3>\n\n\n\n

And that\u2019s it. A very simple way to reverse the system animation for presenting and dismissing view controllers through the navigation controller\u2019s present<\/i> method. That also ends the tutorial. Below are a few notes summarising everything:<\/p>\n\n\n\n