August 21, 2016

iOS: Creating a Custom UIViewController Transition Animation – a Step-By-Step Guide

First of all - what is this 'Transition animation', you might ask? Well, no rocket science here - transition animations provide visual feedback about changes to your app’s interface. UIKit provides a set of standard transition styles to use when presenting view controllers, and you can supplement the standard transitions with custom transitions of your own.You may have seen a lot of transition animations that look really nice and neat but you might not have any idea of how to create one. Some of them look really complicated and some really simple, but in the end you just don't know where to start.

That's why I am going to introduce you to custom transitions. In this step-by-step guide, we are going to create a regular custom transition animations and, in doing so, wash away any fears you might have had about them.
So let's get started!

Starter project

I have already prepared a demo project that you can clone here: https://github.com/willhaben/CustomTransition
This tutorial, as well as the project, is going to be written in Swift 2.2.
In case you don't want to use the project that I have provided, you can also create your own. Just make sure to distinguish two UIViewControllers by including some kind of background image or colour so that the transitions are nicely visible.

What is it going to look like?

We are going to start with a demo project and gradually add code to it so that, in the end, we get a transition animation like this one:

Creating a base animator class

First things first: we are going to create an 'Animator' object, that is going to be our delegate for animations, so that we can make it reusable for all parts of our apps.

Create a new Swift file, name it 'TransitionAnimator' and in the same file create a class with the same name, which is a subclass of NSObject.
import UIKit
final class TransitionAnimator: NSObject {
                                                                                                          
}
Now we are ready to conform to the UIViewControllerAnimatedTransitioning protocol in order to trigger custom transition animations.

When conforming to this protocol we need to implement 2 methods:
  • public func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
  • public func animateTransition(transitionContext: UIViewControllerContextTransitioning)
The first one is pretty self-explanatory - you return the total duration of our custom animation.
The second one should contain the actual animation logic.

So let's extend our Animator object to conform to this protocol as well as to implement two methods and return our animation duration as a private let kAnimationDuration constant with value of 0.6.
final class TransitionAnimator: NSObject {
    private let kAnimationDuration: NSTimeInterval = 0.6
}

extension TransitionAnimator: UIViewControllerAnimatedTransitioning {
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        return kAnimationDuration
    }

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

    }
}
Alright, we are almost ready to start with the animation logic, except that we lack one detail - we have to differentiate between presenting and dismissing animations. After all, we want to have different animations for when we present a viewController and when we are dismissing one, right?
To achieve that we can create a little code>enum that is going to nicely encapsulate that information and then make it a private let transitionMode constant which is initialized through our custom init method:
@objc enum StackModalTransitionMode: Int {
    case Present, Dismiss
    private func isPresenting() -> Bool {
        return self == .Present
    }
}

final class TransitionAnimator: NSObject {
    private let kAnimationDuration: NSTimeInterval = 0.6
    private let transitionMode: StackModalTransitionMode
    // MARK: Lifecycle
    init(transitionMode: StackModalTransitionMode) {
        self.transitionMode = transitionMode
        super.init()
    }
}

extension TransitionAnimator: UIViewControllerAnimatedTransitioning {
...
Here we have marked our enum with an @objc attribute; that is in case we want to use this enum from an objective-c file. Our initializer takes one argument, StackModalTransitionMode, that specifies which type of animation we are willing to trigger - presentation or dismissal.

Creating the necessary animation constants

Now the very last thing we need (it really is the last one before we're starting, I promise) is to create some constant properties that we are going to be using for our animation logic. I don't like having a lot of "magic numbers" dangling around in my code, so I always extract them into dedicated constants with some meaningful names.
...
final class TransitionAnimator: NSObject {
    private let kAnimationDuration: NSTimeInterval = 0.6
    private let kAnimationFirstVisibleViewDelay: NSTimeInterval = 0.0
    private let kAnimationSecondVisibleViewDelay: NSTimeInterval = 0.1
    private let kAnimationHarderSpringDamping: CGFloat = 0.7
    private let kAnimationSmootherSpringDamping: CGFloat = 0.85
    private let kAnimationDismissSpringDamping: CGFloat = 1.0
    private let kAnimationSlowerSpringVelocity: CGFloat = 1.0
    private let kAnimationFasterSpringVelocity: CGFloat = 2.5
    private let kTransformViewScale: CGFloat = 0.95;
    private let transitionMode: StackModalTransitionMode
...
Don't go into too much detail as to why the constants are named that way or why I have that many of them - everything will start to make sense as soon as we are using them in our animation logic.

Filling animateTransition with some logic

When an animation is being triggered, you receive a transitionContext argument which contains information about the viewController that is about to be presented as well as the viewController that presents it. Now, to get the animation effect we want, we have to manipulate views or viewControllers and for that we need transitionContext.

View to squeeze

Let's create a private extension for our class where we will create a helper method that is going to try to extract a view that we need to squeeze for our animation:
...

    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

    }
}

private extension TransitionAnimator {
    func snapshotToSqueeze(withContext transitionContext: UIViewControllerContextTransitioning) -> UIView? {
        let key = transitionMode.isPresenting() ? UITransitionContextFromViewControllerKey : UITransitionContextToViewControllerKey
        guard let viewToSqueeze = transitionContext.viewControllerForKey(key)?.view else { return nil }
        guard let containerView = transitionContext.containerView() else { return nil }
        let snapshotToSqueeze = viewToSqueeze.snapshotViewAfterScreenUpdates(false)
        let background = backgroundView(withFrame: viewToSqueeze.frame)
        containerView.addSubview(background)
        containerView.addSubview(snapshotToSqueeze)
        return snapshotToSqueeze
    }

    func backgroundView(withFrame frame: CGRect) -> UIView {
        let background = UIView(frame: frame)
        background.backgroundColor = .whiteColor()
        let backgroundOverlay = UIView(frame: background.frame)
        backgroundOverlay.backgroundColor = UIColor(white: 0.0, alpha: 0.7)
        background.addSubview(backgroundOverlay)
        return background
    }
}
First, depending on the transitionMode we select the proper key for our viewController that we are going to squeeze and save that string into the key constant.
Next, we use guard for extracting the view we are going to squeeze. We are using guard here as the method viewControllerForKey returns an optional UIView? value. We return nil so that a little bit later we would be able to properly back-out from the animation transition in our code.
After that, we use one more guard for the containerView that we have to use for all our UIViews we want to manipulate in our animation transition - this is an object to add them to. Then we create a screenshot of the view we are going to squeeze.
Now we create a backgroundView that is going to be visible underneath our view we are about to squeeze. I have moved code for this view into a separate method so that the code is more easily readable.
As a next step, first we are adding our backgroundView to the containerView and then snapshotToSqueeze on top of that, so that when we are going to squeeze it, we will see a backgroundView beneath it.
Finally we return snapshotToSqueeze to the caller.
Now let's call this method from our animateTransition method:
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    guard let snapshotToSqueeze = snapshotToSqueeze(withContext: transitionContext) else {
        transitionContext.completeTransition(false)
        return 
    }
}
I guess you've noticed that we are using guard as well, so in the else clause we are calling the transitionContext.completeTransition(false) method, which in that case means that our animation went wrong and we have completed our custom transition. This is very important, because your view is going to be unresponsive otherwise if you don't do this. The reason for this is simple: if something go wrong, we'd better play it safe and think about the user experience.

View to slide

Now it's time to get the view we are going to present or dismiss. To do so, we have to create another helper method named viewToSlide in our private extension next to our previous one:
...
    return snapshotToSqueeze
}

func viewToSlide(withContext transitionContext: UIViewControllerContextTransitioning) -> UIView? {
    let key = transitionMode.isPresenting() ? UITransitionContextToViewKey : UITransitionContextFromViewKey
    guard let viewToSlide = transitionContext.viewForKey(key) else { return nil }
    guard let containerView = transitionContext.containerView() else { return nil }

    containerView.addSubview(viewToSlide)

    return viewToSlide
}

func backgroundView(withFrame frame: CGRect) -> UIView {
...
Here we are doing pretty much the same thing as in our viewToSqueeze method, but this time we don't need a backgroundView as we are going to slide it over another view. That is why we simply add it to our containerView and return viewToSlide back to our animateTransition method. It will now look like this when we add a call for this method here:
...
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    guard let snapshotToSqueeze = snapshotToSqueeze(withContext: transitionContext) else {
        transitionContext.completeTransition(false)
        return
    }
    guard let viewToSlide = viewToSlide(withContext: transitionContext) else {
 transitionContext.completeTransition(false)
 return
    }
}
...
Once again, don't forget to call transitionContext.completeTransition(false) in case something goes wrong - we don't want to block views!

Preparing for animation

We are ready to use the views we have prepared and now we need to position them properly before rendering the animations. This part is not hard - depending on the transitionMode, we are either moving our view we are about to present all the way down, so that we can pull it back up with the animation, or we are squeezing the view that is underneath our presented one so that we can un-squeeze it back with the animation.
...
    return viewToSlide
}

func prepareForAnimation(forSqueezeRelatedSnapshot snapshot: UIView, viewToSlide: UIView) {
    if transitionMode.isPresenting() {
 viewToSlide.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(viewToSlide.bounds))
    } else {
 snapshot.transform = CGAffineTransformMakeScale(kTransformViewScale, kTransformViewScale)
    }
}
...
Then, we add a call to this method back to animateTransition:
...
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    guard let snapshotToSqueeze = snapshotToSqueeze(withContext: transitionContext) else {
 transitionContext.completeTransition(false)
 return
    }
    guard let viewToSlide = viewToSlide(withContext: transitionContext) else {
 transitionContext.completeTransition(false)
 return
    }

    prepareForAnimation(forSqueezeRelatedSnapshot: snapshotToSqueeze, viewToSlide: viewToSlide)
}
...


Animation

Alright, we are almost there, we are ready to actually write animation code, yes! So let's create one method where we decide which animation method to call - the one for presenting or the one for dismissing viewCotroller:
...
func startAnimation(forSqueezeRelatedSnapshot snapshot: UIView, viewToSlide: UIView, transitionContext: UIViewControllerContextTransitioning) {
    let completionBlock: (Bool) -> Void = { finished in
 transitionContext.completeTransition(finished)
    }

    if transitionMode.isPresenting() {
 startAnimationForPresentingMode(withSnapshotToSqueeze: snapshot, viewToSlide: viewToSlide, completionBlock: completionBlock)
    } else {
 startAnimationForDismissingMode(withSnapshotToSqueeze: snapshot, viewToSlide: viewToSlide, completionBlock: completionBlock)
    }
}

func backgroundView(withFrame frame: CGRect) -> UIView {
...
We need to add this call to our animateTransition method, which is going to be the last one there:
...
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
    guard let snapshotToSqueeze = snapshotToSqueeze(withContext: transitionContext) else {
 transitionContext.completeTransition(false)
 return
    }
    guard let viewToSlide = viewToSlide(withContext: transitionContext) else {
 transitionContext.completeTransition(false)
 return
    }

    prepareForAnimation(forSqueezeRelatedSnapshot: snapshotToSqueeze, viewToSlide: viewToSlide)
    startAnimation(forSqueezeRelatedSnapshot: snapshotToSqueeze, viewToSlide: viewToSlide, transitionContext: transitionContext)
}
...
At this point, you should now have two warnings for the unimplemented methods startAnimationForPresentingMode and startAnimationForDismissingMode. Well, let's begin with the presenting one:
...
        startAnimationForDismissingMode(withSnapshotToSqueeze: snapshot, viewToSlide: viewToSlide, completionBlock: completionBlock)
    }
}

func startAnimationForPresentingMode(withSnapshotToSqueeze snapshot: UIView, viewToSlide: UIView, completionBlock: (Bool) -> Void) {
    UIView.animateWithDuration(kAnimationDuration,
                               delay: kAnimationFirstVisibleViewDelay,
                               usingSpringWithDamping: kAnimationHarderSpringDamping,
                               initialSpringVelocity: kAnimationSlowerSpringVelocity,
                               options: .CurveEaseOut,
                               animations: {
 snapshot.transform = CGAffineTransformMakeScale(self.kTransformViewScale, self.kTransformViewScale)
    }, completion: nil)

    UIView.animateWithDuration(kAnimationDuration,
                               delay: kAnimationSecondVisibleViewDelay,
                               usingSpringWithDamping: kAnimationSmootherSpringDamping,
                               initialSpringVelocity: kAnimationFasterSpringVelocity,
                               options: .CurveEaseOut,
                               animations: {
 viewToSlide.transform = CGAffineTransformIdentity
    }, completion: completionBlock)
}
...
Here we are simply using two UIView animation methods - one for our snapshot a.k.a view we are going to squeeze and another one for the view that is going to slide up. And here you can finally see the constants we have created at the beginning of this tutorial in use.
Let's add the second animation method as well, that we are going to use once we are dismissing our presented viewController.
...
 viewToSlide.transform = CGAffineTransformIdentity
    }, completion: completionBlock)
}

func startAnimationForDismissingMode(withSnapshotToSqueeze snapshot: UIView, viewToSlide: UIView, completionBlock: (Bool) -> Void) {
    UIView.animateWithDuration(kAnimationDuration,
                            delay: kAnimationFirstVisibleViewDelay,
                            usingSpringWithDamping: kAnimationDismissSpringDamping,
                            initialSpringVelocity: kAnimationSlowerSpringVelocity,
                            options: .CurveEaseIn,
                            animations: {
        viewToSlide.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(viewToSlide.bounds))
    }, completion: nil)

    UIView.animateWithDuration(kAnimationDuration,
                               delay: kAnimationSecondVisibleViewDelay,
                               usingSpringWithDamping: kAnimationDismissSpringDamping,
                               initialSpringVelocity: kAnimationSlowerSpringVelocity,
                               options: .CurveEaseIn,
                               animations: {
 snapshot.transform = CGAffineTransformIdentity
    }, completion: completionBlock)
}
...


Connecting TransitionAnimator to UIViewController

Now that we have completed all the animation related stuff, we have one last thing to do before we will be able to actually see the result on our devices - we have to explicitly state that we want to use a custom transition animation and that our TransitionAnimator is going to do all the animation work. In order to do so, you have to force the viewController that is involved in the animation to conform to the UIViewControllerTransitioningDelegate protocol. Once conforming to this protocol, it is necessary to implement two pretty much self explanatory methods: animationControllerForPresentedController and animationControllerForDismissedController. In both cases we are going to use our TransitionAnimator as we have configured it to handle both presenting and dismissing animations through a parameter that we pass along in the init method. Also, as we are specifying our TransitionAnimator as primary for both animations through delegate methods.
Now, go to the first viewController that presents the second one. In case you are using the demo project I have provided, then this class is called FirstViewController. Open it and add conformance to protocol as an extension to our class along with two methods.
...
extension FirstViewController: UIViewControllerTransitioningDelegate {

    func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
 return TransitionAnimator(transitionMode: .Present)
    }

    func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
 return TransitionAnimator(transitionMode: .Dismiss)
    }
}
Now in order for this function to be actually called we have to do one last part - prior to the presentViewController method call we have to specify that we want to use the custom transition animation. For that case, these two methods from the UIViewControllerTransitioningDelegate protocol are going to be called.
Navigate to the didTapPresentButton method in the FirstViewController class and add these two lines right after the guard line:

@IBAction func didTapPresentButton(sender: UIButton) {
    guard let secondVC = storyboard?.instantiateViewControllerWithIdentifier(String(SecondViewController.self)) else { return }
    secondVC.modalPresentationStyle = .Custom
    secondVC.transitioningDelegate = self
    presentViewController(secondVC, animated: true, completion: nil)
}

Conclusion

So as you can see from all the steps above, it is not hard to do this at all. However, creating custom transition animations definitely takes time, especially when you start playing around with animation parameters. That's all from me now - enjoy!

8 comments:

  1. Thank you so much for this step by step guide. Really very helpful..It is just what I was looking for . Thank you so much once again.

    ReplyDelete
  2. This instrument to activitys is attempted as "most constrained apparatus" up to now out there. some of the examples of the 3D toon activity square measure most recent energized films like Avatar. More about the author

    ReplyDelete
  3. I would like to know more about the UI View Controller Transition Animation.Is this is really a simple way to prepare the best animated videos.

    ReplyDelete
  4. The step by step procedure explained to create a custom UI view controller is a relevant discussing area. I got few ideas about it and I wish to know more.

    ReplyDelete
  5. Hope it will be more effective and helpful for those who are completely new to transition animation. This blog gives more ideas about custom transitions and the steps that we can follow for making transition animations.

    ReplyDelete
  6. You can also view here https://essay4today.com/ lots of great articles on this thematic of writing.

    ReplyDelete