Making an interactive dismissal transition

Implementing an interactive transition requires a bit more work than the non-interactive version, and the way it works is also somewhat harder to grasp. The non-interactive transition worked by returning the object that took care of the animations in the animationController(forPresentedController:presenting:source) method.

For the interactive dismiss transition, two methods should be implemented. These two methods work together to make the interactive animation happen. The first method is animationController(forDismissedController:). This method will return an object that will perform animations, just like its counterpart that is called to present a view controller. However, to make the animation interactive, we must also implement the interactionController(forDismissal:) method. This method should return an object that works in conjunction with the object we returned from animationController(forDismissedController:). The way this all ties together can roughly be summed up as follows:

  1. A UIViewControllerAnimatedTransitioning object is requested by calling animationController(forDismissedController:).
  1. A UIViewControllerInteractiveTransitioning object is requested by calling interactionController(forDismissal:). The UIViewControllerAnimatedTransitioning object that was retrieved earlier is passed to this method. If this method returns nil, the transition will be executed without being interactive.
  2. If both methods return a valid object, the transition is interactive.

Let's take a look at how this compares to the previous animation flow we looked at earlier in the following diagram:

For convenience, we'll implement both UIViewControllerAnimatedTransitioning and UIViewControllerInteractiveTransitioning in a single class. This will make it a little bit easier to see how everything ties together.

Start off by creating a new Cocoa Touch class and name it CustomModalHideAnimator. Choose UIPercentDrivenInteractiveTransition as its superclass. This class implements convenience methods to easily update the interactive transition. It also conforms to UIViewControllerInteractiveTransitioning, so we don't have to add conformance ourselves. However, you should make sure to add conformance to the UIViewControllerAnimatedTransitioning to CustomModalHideAnimator declaration yourself.

Let's start off by implementing a custom initializer that will tie the CustomPresentedViewController instance to CustomModalHideAnimator. This enables us to add a gesture recognizer to the modal view and update the animation based on the status of the gesture recognizer. Add the following code to the implementation for CustomModalHideAnimator:

let viewController: UIViewController 

init(withViewController viewController: UIViewController) {
self.viewController = viewController

super.init()

let panGesture = UIScreenEdgePanGestureRecognizer(
target: self,
action: #selector(handleEdgePan(gestureRecognizer:)))

panGesture.edges = .left
viewController.view.addGestureRecognizer(panGesture)
}

@objc func handleEdgePan(gestureRecognizer: UIScreenEdgePanGestureRecognizer) {
let panTranslation = gestureRecognizer.translation(in: viewController.view)
let animationProgress = min(max(panTranslation.x / 200, 0.0), 1.0)

switch gestureRecognizer.state {
case .began:
viewController.dismiss(animated: true, completion: nil)
case .changed:
update(animationProgress)
break
case .ended:
if animationProgress < 0.5 {
cancel()
} else {
finish()
}
break
default:
cancel()
break
}
}

This snippet starts off with a custom initializer that immediately ties a UIViewController instance to itself. Then it completes initialization by calling the superclass's initializer, and then the pan gesture is added to the view. We're using UIScreenEdgePanGestureRecognizer so we can bind it to swiping from the left edge of the screen. This mimics the standard gesture that's normally used to go back a step.

Next, the handleEdgePan(_:) method is implemented. This method figures out the distance that is swiped. Then, the state of the gesture is checked. If the user just started the gesture, we tell the view controller to dismiss it. This will trigger the sequence of steps that was outlined before, and it will start the interactive dismissal.

If the gesture just changed, the animation's progress is updated by calling the update(_:) method of UIPercentDrivenInteractiveTransition. If the gesture ended, we check the progress made so far. If there is enough progress, we finish the transition; otherwise, we cancel it. If we receive any other status for the gesture, we assume it got canceled so we also cancel the transition. If you noticed some similarities between this implementation and the interactive UIViewPropertyAnimator example you saw before, you're not wrong! These implementations are pretty similar in terms of tracking a gesture and updating an animation's progress.

Next, we implement the UIViewControllerAnimatedTransitioning methods that describe the transition we're executing. This transition basically does the opposite from the transition we used to display our modal view controller. The following snippet implements this:

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 
return 0.6
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

guard let fromViewController = transitionContext.viewController(
forKey: UITransitionContextViewControllerKey.from),
let toViewController = transitionContext.viewController(
forKey: UITransitionContextViewControllerKey.to) else {
return
}

let transitionContainer = transitionContext.containerView

transitionContainer.addSubview(toViewController.view)
transitionContainer.addSubview(fromViewController.view)

let animationTiming = UISpringTimingParameters(
dampingRatio: 0.8,
initialVelocity: CGVector(dx: 1, dy: 0))

let animator = UIViewPropertyAnimator(
duration: transitionDuration(using: transitionContext),
timingParameters: animationTiming)

animator.addAnimations {
var transform = CGAffineTransform.identity
transform = transform.concatenating(CGAffineTransform(scaleX:
0.6, y: 0.6))
transform = transform.concatenating(CGAffineTransform(translationX: 0, y: -200))

fromViewController.view.transform = transform
fromViewController.view.alpha = 0
}

animator.addCompletion { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
}

animator.startAnimation()
}

If you study this code, you'll find that it's not very different from its counterpart responsible for displaying the modal view. All that's left to do now is to make sure that our CustomPresentedViewController uses this custom animation to create an instance of our CustomModalHideAnimator and implement the interactionController(forDismissal:) and animationController(forDismissedController:) methods. Replace the current viewDidLoad implementation in your CustomPresentedViewController with the following code:

var hideAnimator: CustomModalHideAnimator? 

override func viewDidLoad() {
super.viewDidLoad()

transitioningDelegate = self
hideAnimator = CustomModalHideAnimator(withViewController: self)
}

The preceding code creates an instance of CustomModalHideAnimator and binds the view controller to it by passing it to the initializer. Next, update the code in animationController(forDismissedController:) so it returns hideAnimator instead of nil, as follows:

func animationController(forDismissedController dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 
return hideAnimator
}

Finally, implement the interactionController(forDismissal:) method so the transition becomes interactive, as follows:

func interactionController(forDismissal animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 
return hideAnimator
}

Try to run your app now and swipe from the left edge of the screen once you've presented your custom modal view. You can now interactively make the view go away by performing a gesture. Clever implementations of custom transitions can really make users feel in control of your application and the way it responds to them.

Implementing a custom transition is a task that isn't easy by any means. There are a lot of moving parts involved and the amount of delegation and the number of protocols used can be daunting. Take the time to go over the code you've written a few more times to figure out what exactly is going on if you need to. Again, this isn't an easy topic to understand or grasp.