Building a Custom Transitioning for ViewController in iOS

Let’s build custom transitions into our appsybill bed

Arjun Baru
May 26 · 5 min read
Photo by Wesson Wang on Unsplash

Overview

Every new iOS comes with design changes. This time we saw the new View Presentation Style, which gives a 3D look to a view controller.

Although Swift is open-source, Apple’s framework isn’t. You might be wondering how apple did it — I certainly was. I’ve discovered that it’s not a black box.

In this article, we’ll try to recreate what Apple has been cooking — AKA cracking the iOS Code!

Prerequisites

Be familiar with UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning. If you’re not, you’d better check out this Ray Wenderlich article first.

Getting Started

Download the project.

Let’s explore the starter project. Build and run:

iOS 13 Presentation

Looking through the code, we have a PresentingViewController class with the Show Cities button and a PresentedViewController class which shows some city data.

If you’re wondering how to animate Navigation Bar, check out Build an Animating UINavigationBar in Swift.

Let’s Code

We’ll start by adding CustomAnimatedController.swift to project.

First, add this helper enum in the file:

enum ViewControlerScale {
case modelPresentationScale
case reset
var transform: CATransform3D {
switch self {
case .modelPresentationScale:
return CATransform3DMakeScale(0.88, 0.88, 1)
case .reset:
return CATransform3DMakeScale(1, 1, 1)
}
}
}

This will help us to achieve background shrink when new viewController pops up and reset it once viewController dismiss. You will have a better idea once we use it.

Now, let’s add the below class:

//1
class CustomAnimatedController: NSObject, UIViewControllerAnimatedTransitioning {
//2
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {}
//3
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {}
}

//1 We started with creating a CustomAnimatedController which conforms to UIViewControllerAnimatedTransitioning. This delegate has two mandatory methods (2 and 3).

//2 This function asks for the time duration for the animation.

//3 This is where we add the magic. This function is responsible for transitioning from current ViewController to future ViewController.

As per the above understanding, what does this class need? Duration (for delegate), estimated final height, and whether we’re presenting or dismissing the viewController (we’ll come to this part later).

Add the below code to the class:

let estimatedFinalHeight: CGFloat
let animationDuration: TimeInterval
var isPresenting: Bool
init(estimatedFinalHeight: CGFloat, animationDuration: TimeInterval, isPresenting: Bool) {self.estimatedFinalHeight = estimatedFinalHeight
self.animationDuration = animationDuration
self.isPresenting = isPresenting
}

Let’s explore func animateTransition(using transitionContext: UIViewControllerContextTransitioning).

Add the following code:

// 1
guard let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to) else { return }
//2
let containerView = transitionContext.containerView
//3
fromVC.beginAppearanceTransition(false, animated: true)
toVC.beginAppearanceTransition(true, animated: true)

//1 This line will give us both the view controllers(to and from).

//2 The container view is responsible for the animation. We add the view which will be presenting and dismissing to animate it accordingly.

//3 When we do custom presentation, our viewController’s lifecycle doesn’t get called which will result in abrupt behaviour. It’s recommended that we never call lifecycle methods directly. This function takes care of that.

Let’s add code in order to present a view controller on top of another:

if isPresenting {// 1
toVC.view.frame.origin.y = fromVC.view.frame.maxY
containerView.addSubview(toVC.view)
UIView.animate(withDuration: animationDuration, animations: {// 2
toVC.view.frame.origin.y = self.estimatedFinalHeight
fromVC.view.layer.transform =
ViewControlerScale.modelPresentationScale.transform
}, completion: { _ in//3
fromVC.endAppearanceTransition()
toVC.endAppearanceTransition()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}

//1 We will fix the “To be presented” ViewController at the bottom of the currently presented ViewController. Add toVC to Container view(as this is responsible for holding the presented ViewController).

//2 Within Animation we are changing the Y Axis of toVC from maxY to the desired height. Also, with the help enum, declared above, we transform our background ViewController to scale/shrink to give a 3D effect.

//3 Once we’re done with animation, we notify system so that lifecycle methods can be called properly.

If not presenting, it should be dismissing. Let’s add the else part in continuity to the above code:

else {// 1
containerView.addSubview(fromVC.view)
UIView.animate(withDuration: animationDuration, animations: {// 2
fromVC.view.frame.origin.y = UIScreen.main.bounds.maxY
toVC.view.layer.transform = ViewControlerScale.reset.transform
}, completion: { _ in// 3
fromVC.endAppearanceTransition()
toVC.endAppearanceTransition()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}

The catch: We’re also using the same function to dismiss ViewController. At this time, we’re going from PresentedViewController (which will now be fromVC) to presentingViewController (which will be toVC). The tables have turned!

//1 We are translating the pop-up ViewController to the bottom before it gets dismissed. This view gets added to containerView.

//2 Reset the toVC (PresentingViewController) which we scaled down previously to its original scale.

//3 Let the system know we are done with Animation so that appropriate lifecycle methods get called.

Setting up the Transitioning Delegate Class

Let’s add another file — CustomTransitioningController.swift — and add the following code to it:

//1 This class will take up the initial heigh and duration which will then be passed to our CustomAnimatedController class. Also, this is the front liner class which will be consumed by our Viewcontrollers.

//2 We will have a private var animatorController which will be guiding the presentation and dismissal of the view controllers.

Our CustomTransitioningController class conforms to delegate UIViewControllerTransitioningDelegate , this delegate comes with two optional functions — let’s look at them.

// 1
func animationController(forPresented presented: UIViewController,
presenting: UIViewController,
source: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
animatorController.isPresenting = true
return animatorController
}// 2
func animationController(forDismissed dismissed: UIViewController)
-> UIViewControllerAnimatedTransitioning? {
animatorController.isPresenting = false
return animatorController
}

//1 The first function guides us for the presenting flow. We get presented (our current VC) and presenting (to be presented VC). Any additional changes to viewController before the animation can be done here.

//2 This function gives us the controller which will be going to dismiss. Any last-minute adjustment can be made to that view in this function.

Time to Connect Our Custom Presentation

Add a property to the PresentingViewController class:

let customTransitioningDelegate = CustomTransitioningController()

Replace the body of onTapOfShowCities function with the below code.

guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(identifier: "PresentedViewController") as? PresentedViewController else { return }vc.modalPresentationStyle = .custom
vc.transitioningDelegate = customTransitioningDelegate
self.present(vc, animated: true, completion: nil)

We set the presentation style to custom and let the system know about our new transitioning delegate.

Build and run!

Just as we expected. But wait, why are the corners so sharp? Let’s fix them.

Go to CustomAnimatedController class and add the following code (mentioned on the breakpoint, line numbers 63 and 77).

We are cornering the radius when presenting and reverting at dismissal.

The Final Result

Custom Presentation.

Great job! We now have something similar to what Apple has. If you observe Apple’s animation, we see swipe to dismiss and a nice fade to background too. Can we do that? Of course, but that's a topic for Part 2 of the tutorial!

Thanks for reading!

Better Programming

Advice for programmers.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store