Color Mode


    Language

Animated Transitions in ViewController - Part 1

February 27, 2020

The hunger for magnificent design, animations and transitions is huge and nowadays designers are pushing developers to create beautiful views with lots of animations and smooth transitions.

Based on this, I will present some basic concepts of transitions and animations. This is the first in a series of three blogposts.

In this article, we will create a custom transition with View Controller Transitions and understand how it works based on UIKit. This is a powerful framework and you can use it to create smooth transitions and animate the elements of your view controller.

And to satisfy this hunger, nothing better than a Pizza App, right? Or maybe a salad if you don't like pizza...

![Salad](/assets/img/articles/2020-02-27-View-Controller-Transitions/EatPizza.webp

By the end of this article you will be able to:

  • Create your custom transition
  • Animate view elements
  • Understand the transition delegates methods

The final result should look something like this 😉

![Pizza Transition](/assets/img/articles/2020-02-27-View-Controller-Transitions/PizzaPart1.webp

You can also clone the final project from here.

Let's get started!

In this example, I'm using a simple ViewController with a UICollectionView. When an item is selected, a second ViewController is presented.

As our PizzaViewController is embedded in a UINavigationController, our ViewController must conform to the protocol UINavigationControllerDelegate. In this case, we are assigning the delegate to self and we will implement this protocol in a second.

override func viewDidLoad() {
    super.viewDidLoad()

    navigationController?.delegate = self

    viewModel.delegate = self
    viewModel.getPizzas()
}

To trigger our animation every time you click in one of the cells, we store the IndexPath in a variable in the ViewModel for future reference, and for presenting the PizzaDetailViewController

extension PizzaViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {

        viewModel.indexPath = indexPath
        let pizza = viewModel.pizzas[indexPath.row]

        let detailVC = PizzaDetailViewController.instantiate()
        detailVC.pizza = pizza

        navigationController?.modalPresentationStyle = .custom
        navigationController?.pushViewController(detailVC, animated: true)
    }
}

So far there's nothing special other than assigning the NavigationController delegate to our ViewController.

Implementing the delegate

This is where things starts getting interesting 😀

We can have different animations, depending on whether you are presenting or dismissing a ViewController. To find out which one you should use, you can check the value of a property called operation with type UINavigationController.Operation. This property will tell us whether the user is presenting or dismissing a view.

In the first part of this example, we will implement only the presentation animation.

Your code should look like this:

extension PizzaViewController: UINavigationControllerDelegate {
    func navigationController(_ navigationController: UINavigationController,
                              animationControllerFor operation: UINavigationController.Operation,
                              from fromVC: UIViewController,
                              to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        switch operation {
        case .push:
            return AnimationManager(animationDuration: 1.5, animationType: .present)
        case .pop:
            return AnimationManager(animationDuration: 1.5, animationType: .dismiss)
        default:
            return nil
        }
    }
}

Notice here that we are returning an object of type UIViewControllerAnimatedTransitioning which is our AnimationManagerclass. We'll cover that later. Hold on!

To make our life easier, both of our ViewControllers implement the protocol PizzaTransitionable. This protocol has two properties and we will use them in our animations. As we are dealing with a CollectionView and IndexPath, it's handy to have this kind of helper.

protocol PizzaTransitionable {
    var backgroundView: UIView { get }
    var pizzaImage: UIImageView { get }
}
Implementing the protocol on PizzaViewController
extension PizzaViewController: PizzaTransitionable {
    var backgroundView: UIView {
        guard
            let indexPath = viewModel.indexPath,
            let pizzaCell = collectionView.cellForItem(at: indexPath) as? PizzaViewCell
        else {
            return UIView()
        }
        return pizzaCell.containerView
    }

    var pizzaImage: UIImageView {
        guard
            let indexPath = viewModel.indexPath,
            let pizzaCell = collectionView.cellForItem(at: indexPath) as? PizzaViewCell
        else {
            return UIImageView()
        }
        return pizzaCell.pizzaImage
    }
}
Implementing the protocol on PizzaDetailViewController
extension PizzaDetailViewController: PizzaTransitionable {
    var backgroundView: UIView {
        return detailBackgroundView
    }

    var pizzaImage: UIImageView {
        return detailPizzaImage
    }
}

Right now we have everything ready to get our hands dirty.

![Excellent](/assets/img/articles/2020-02-27-View-Controller-Transitions/Excellent.webp

Animating the transition 😍

Now it's time to see what's inside of our AnimationManager class.

Here we have two properties animationDuration and animationType. Probably you already know what they mean, right?

final class AnimationManager: NSObject {

    // MARK: - Variables
    private let animationDuration: Double
    private let animationType: AnimationType

    // MARK: - Init
    init(animationDuration: Double, animationType: AnimationType) {
        self.animationDuration = animationDuration
        self.animationType = animationType
    }
}

The AnimationType is a custom enum and it has only 2 cases. We use it to know when we need to animate the presenting or dismissing of a view.

enum AnimationType {
    case present
    case dismiss
}

We also need to make our AnimationManager conform to the UIViewControllerAnimatedTransitioning protocol.

The view controller will call the animator object from the transitioning delegate and will use it to perform the animation as you can see at the end of this snippet code.

extension AnimationManager: UIViewControllerAnimatedTransitioning {
    // Return the animation duration defined when we instantiated the view.
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return TimeInterval(exactly: animationDuration) ?? 0
    }

    // Retrieve the ViewControllers and call the animation method
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard
            let toViewController = transitionContext.viewController(forKey: .to),
            let fromViewController = transitionContext.viewController(forKey: .from)
        else {
            // We only complete transition with success if the transition was executed.
            transitionContext.completeTransition(false)
            return
        }

        // According to the animation type we call the method to animate the presenting or dismissing.
        switch animationType {
        case .present:
            presentAnimation(
                transitionContext: transitionContext,
                fromView: fromViewController,
                toView: toViewController
            )
        case .dismiss:
            dismissAnimation(
                transitionContext: transitionContext,
                fromView: fromViewController,
                toView: toViewController
            )
        }
    }
}

Presenting the animation

For a clean and organized codebase, the logic is split into different chunks of code using extensions. That way the code is more readable and nicer as you can see in the example project! 🤓

But here it is shown separately:

For this animation, the duration is split in two parts. The first one should run in 1/3 of the duration and the second part the other 2/3 of the time.

// Assigning the context to a variable.
let containerView = transitionContext.containerView

// We split the whole animation in 2 different parts.
// The fist part should run in 1/3 of the time and the second part 2/3 of the time.
let firstPartDuration = (animationDuration / 3)
let secondPartDuration = (animationDuration / 3) * 2

Since delegate provides us a ViewController object, we need to cast it into the view type to access its objects.

// Cast the ViewControllers to their original type
guard let fromPizzaVC = fromView as? PizzaViewController else { return }
guard let toPizzaDetailVC = toView as? PizzaDetailViewController else { return }

Instead of animating the real objects of the view, we mirror them by creating new objects assigning the same position as the original ones. That way we can hide the ViewController and play with the "fake" objects.

// Mirroring the objects that we will animate.
let backgroundView = UIView()
let pizzaSnapshot = UIImageView()

// Storing the frame positions.
let backgroundFrame = containerView.convert(
    fromPizzaVC.backgroundView.frame,
    from: fromPizzaVC.backgroundView.superview
)

let pizzaSnapshotFrame = containerView.convert(
    fromPizzaVC.pizzaImage.frame,
    from: fromPizzaVC.pizzaImage.superview
)

// Setting up new objects with original attributes from the presented view controller.
backgroundView.frame = backgroundFrame
backgroundView.backgroundColor = fromPizzaVC.backgroundView.backgroundColor
backgroundView.layer.cornerRadius = fromPizzaVC.backgroundView.layer.cornerRadius

pizzaSnapshot.frame = pizzaSnapshotFrame
pizzaSnapshot.image = fromPizzaVC.pizzaImage.image
pizzaSnapshot.contentMode = .scaleAspectFit

Right after creating our "fake" objects, we need to add it to the containerView, then we can hide/show the objects from the original ViewControllers as you wish.

// Adding the subviews to the container view.
containerView.addSubview(fromPizzaVC.view)
containerView.addSubview(toPizzaDetailVC.view)
containerView.addSubview(backgroundView)
containerView.addSubview(pizzaSnapshot)

// Hidding/Showing objects to not/be displayed while animating the transition.
fromPizzaVC.view.isHidden = false
fromPizzaVC.collectionView.isHidden = true
toPizzaDetailVC.view.isHidden = true

Since the animations are running in a chain, we animate the objects using UIViewPropertyAnimator. That way, we have more control over our animations and the code is more readable.

// Background view final position.
let frameAnim = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)

// Animate the view objects using PropertyAnimator
let animator1 = {
    UIViewPropertyAnimator(duration: firstPartDuration, dampingRatio: 1) {
        backgroundView.transform = CGAffineTransform(scaleX: 0.9, y: 0.9)
    }
}()

let animator2 = {
    UIViewPropertyAnimator(duration: secondPartDuration, curve: .easeInOut) {
        backgroundView.frame = frameAnim
        pizzaSnapshot.transform = CGAffineTransform(rotationAngle: .pi)
        pizzaSnapshot.frame = containerView.convert(
            toPizzaDetailVC.pizzaImage.frame,
            from: toPizzaDetailVC.pizzaImage.superview
        )
    }
}()

And the final step is to define the sequence and start the animation. 🤗

// Prepare the animations sequence
animator1.addCompletion { _ in
    animator2.startAnimation()
}

animator2.addCompletion { _ in
    fromPizzaVC.collectionView.isHidden = false
    toPizzaDetailVC.view.isHidden = false

    backgroundView.removeFromSuperview()
    pizzaSnapshot.removeFromSuperview()

    transitionContext.completeTransition(true)
}

// Run animations
animator1.startAnimation()

Dismissing the animation

As you know, we are not going to implement the dismiss method in this article. But stay tuned because we'll prepare some interesting animations in the future. For now, let's just go back to the PizzaViewController without doing anything.

private extension AnimationManager {
    func dismissAnimation(transitionContext: UIViewControllerContextTransitioning,
                          fromView: UIViewController,
                          toView: UIViewController) {

        let containerView = transitionContext.containerView

        containerView.addSubview(toView.view)
        containerView.addSubview(fromView.view)

        // We'll not animate anything. This will be covered in the next part of the article.
        transitionContext.completeTransition(true)
    }
}

Where to Go From Here?

Now that you've learned some of the basics animations when transitioning your ViewControllers you can try to improve and make some incredible transitions! The possibilities are endless!!

Resources

  • Customizing the Transition Animations

Photo by Rodolfo Clix from Pexels

iosswiftanimationuikit

Author

Ricardo Tokashiki

Ricardo Tokashiki

iOS Developer

Food lover 🍣, tennis player 🎾 and coder 👨‍💻

You may also like

November 7, 2024

Introducing Shorebird, code push service for Flutter apps

Update Flutter apps without store review What is Shorebird? Shorebird is a service that allows Flutter apps to be updated directly at runtime. Removing the need to build and submit a new app version to Apple Store Connect or Play Console for review for ev...

Christofer Henriksson

Christofer Henriksson

Flutter

May 27, 2024

Introducing UCL Max AltPlay, a turn-by-turn real-time Football simulation

At this year's MonstarHacks, our goal was to elevate the sports experience to the next level with cutting-edge AI and machine learning technologies. With that in mind, we designed a unique solution for football fans that will open up new dimensions for wa...

Rayhan NabiRokon UddinArman Morshed

Rayhan Nabi, Rokon Uddin, Arman Morshed

MonstarHacks

ServicesCasesAbout Us
CareersThought LeadershipContact
© 2022 Monstarlab
Information Security PolicyPrivacy PolicyTerms of Service