UIPresentationController Tutorial: Getting Started

Learn how to build custom view controller transitions and presentations with this UIPresentationController tutorial. By Ron Kliffer.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Creating the Animation Controller

To add a custom animation transition, you’ll create a subclass of NSObject that conforms to UIViewControllerAnimatedTransitioning.

For complex animations you’d usually create two controllers — one for presentation and one for dismissal. In the case of this app, dismissal mirrors presentation, so you only need one animation controller.

Go to File ▸ New ▸ File…, choose iOS ▸ Source ▸ Cocoa Touch Class and click Next. Set the name to SlideInPresentationAnimator, make it a subclass of NSObject and set the language to Swift.

Click Next and set the group to Presentation and then click Create to make your new file. Open SlideInPresentationAnimator.swift and replace its contents with the following:

import UIKit

final class SlideInPresentationAnimator: NSObject {
  // 1
  // MARK: - Properties
  let direction: PresentationDirection

  //2
  let isPresentation: Bool
  
  //3
  // MARK: - Initializers
  init(direction: PresentationDirection, isPresentation: Bool) {
    self.direction = direction
    self.isPresentation = isPresentation
    super.init()
  }
}

Here you declare:

  1. direction that tells the animation controller the direction from which it should animate the view controller’s view.
  2. isPresentation to tell the animation controller whether to present or dismiss the view controller.
  3. An initializer that accepts the two declared values above.

Next, add conformance to UIViewControllerAnimatedTransitioning by adding the following extension:

// MARK: - UIViewControllerAnimatedTransitioning
extension SlideInPresentationAnimator: UIViewControllerAnimatedTransitioning {
  func transitionDuration(
    using transitionContext: UIViewControllerContextTransitioning?
  ) -> TimeInterval {
    return 0.3
  }

  func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning
  ) {}
}

The protocol has two required methods — one to define how long the transition takes (0.3 seconds in this case) and one to perform the animations. The animation method is a stub to keep the compiler happy.

Replace the animateTransition(using:) stub with the following:

func animateTransition(
    using transitionContext: UIViewControllerContextTransitioning) {
  // 1
  let key: UITransitionContextViewControllerKey = isPresentation ? .to : .from

  guard let controller = transitionContext.viewController(forKey: key) 
    else { return }
    
  // 2
  if isPresentation {
    transitionContext.containerView.addSubview(controller.view)
  }

  // 3
  let presentedFrame = transitionContext.finalFrame(for: controller)
  var dismissedFrame = presentedFrame
  switch direction {
  case .left:
    dismissedFrame.origin.x = -presentedFrame.width
  case .right:
    dismissedFrame.origin.x = transitionContext.containerView.frame.size.width
  case .top:
    dismissedFrame.origin.y = -presentedFrame.height
  case .bottom:
    dismissedFrame.origin.y = transitionContext.containerView.frame.size.height
  }
    
  // 4
  let initialFrame = isPresentation ? dismissedFrame : presentedFrame
  let finalFrame = isPresentation ? presentedFrame : dismissedFrame
    
  // 5
  let animationDuration = transitionDuration(using: transitionContext)
  controller.view.frame = initialFrame
  UIView.animate(
    withDuration: animationDuration,
    animations: {
      controller.view.frame = finalFrame
  }, completion: { finished in
    if !self.isPresentation {
      controller.view.removeFromSuperview()
    }
    transitionContext.completeTransition(finished)
  })
}

I did say this one does the heavy lifting! Here’s what each section does:

  1. If this is a presentation, the method asks the transitionContext for the view controller associated with .to. This is the view controller you’re moving to. If dismissal, it asks the transitionContext for the view controller associated with .from. This is the view controller you’re moving from.
  2. If the action is a presentation, your code adds the view controller’s view to the view hierarchy. This code uses the transitionContext to get the container view.
  3. Calculate the frames you’re animating from and to. The first line asks the transitionContext for the view’s frame when it’s presented. The rest of the section tackles the trickier task of calculating the view’s frame when it’s dismissed. This section sets the frame’s origin so it’s just outside the visible area based on the presentation direction.
  4. Determine the transition’s initial and final frames. When presenting the view controller, it moves from the dismissed frame to the presented frame — vice versa when dismissing.
  5. Lastly, this method animates the view from initial to final frame. If this is a dismissal, you remove the view controller’s view from the view hierarchy. Note that you call completeTransition(_:) on transitionContext to inform the transition has finished.

Wiring Up the Animation Controller

You’re at the last step of building the transition: Hooking up the animation controller!

Open SlideInPresentationManager.swift and add the following two methods to the end of UIViewControllerTransitioningDelegate extension:

func animationController(
  forPresented presented: UIViewController,
  presenting: UIViewController,
  source: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
  return SlideInPresentationAnimator(direction: direction, isPresentation: true)
}

func animationController(
  forDismissed dismissed: UIViewController
) -> UIViewControllerAnimatedTransitioning? {
  return SlideInPresentationAnimator(direction: direction, isPresentation: false)
}

The first method returns the animation controller for presenting the view controller. The second returns the animation controller for dismissing the view controller. Both are instances of SlideInPresentationAnimator, but they have different isPresentation values.

Build and run the app. Check out those transitions! You should see a smooth slide-in animation from the left when you tap Summer. For Winter, it comes from the right, and Medal Count comes from the bottom.

medal_gif_03

Your result is exactly what you set out to do!

It works great… as long as the device is in portrait orientation. Try rotating to landscape.

Adaptivity

The good news is that you’ve done the hardest part! The transitions work perfectly. In this section, you’ll make the effect work beautifully on all devices and both orientations.

Build and run the app again, but this time run it on an iPhone SE. You can use the simulator if you don’t have the actual device. Try opening the Summer menu in landscape. See anything wrong here?
medal_count_07
Well, no. This actually looks great! Take a victory lap around your desk.

But what happens when you try to bring up the medal count? Select a year from the menu and tap Medal Count. You should see the following screen:
medal_count_08
SlideInPresentationController restricts the view to 2/3 of the screen, leaving little space to show the medal count view. If you ship the app like this, you’re sure to hear complaints.

Fortunately for you, adaptivity is a thing. The iPhone has .regular height size class in portrait and .compact height size class in landscape. All you have to do is make a few alterations to the presentation to make use of this feature!

UIPresentationController has a delegate that conforms to UIAdaptivePresentationControllerDelegate, and it defines several methods to support adaptivity. You’ll use two of them in a moment.

First, you’ll make SlideInPresentationManager the delegate of SlideInPresentationController. This is the best option because the controller you choose to present determines whether the app should support compact height or not.

For example, GamesTableViewController looks correct in compact height, so there’s no need to limit its presentation. However, you do want to adjust the presentation for MedalCountViewController.

Open SlideInPresentationManager.swift and add the following below direction:

var disableCompactHeight = false

Here you add disableCompactHeight to indicate if the presentation supports compact height.

Next, add an extension that conforms to UIAdaptivePresentationControllerDelegate and implements adaptivePresentationStyle(for:traitCollection:) as follows:

// MARK: - UIAdaptivePresentationControllerDelegate
extension SlideInPresentationManager: UIAdaptivePresentationControllerDelegate {
  func adaptivePresentationStyle(
    for controller: UIPresentationController,
    traitCollection: UITraitCollection
  ) -> UIModalPresentationStyle {
    if traitCollection.verticalSizeClass == .compact && disableCompactHeight {
      return .overFullScreen
    } else {
      return .none
    }
  }
}

This method accepts a UIPresentationController and a UITraitCollection and returns the desired UIModalPresentationStyle.

Next, it checks if verticalSizeClass equals .compact and if compact height is disabled for this presentation.

  • If yes, it returns a presentation style of .overFullScreen. This way, the presented view will cover the entire screen — not just 2/3 as defined in SlideInPresentationController.
  • If no, it returns .none, to stay with the implementation of UIPresentationController.

Find presentationController(forPresented:presenting:source:).
Set SlideInPresentationManager as the presentation controller’s delegate by adding the following line above the return statement:

presentationController.delegate = self

Finally, you’ll tell SlideInPresentationManager when to disable compact height.

Open MainViewController.swift and locate prepare(for:sender:). Find where the segue’s destination view controller is GamesTableViewController, and then add the following line to the if block:

slideInTransitioningDelegate.disableCompactHeight = false

Find where the segue’s destination view controller is MedalCountViewController and add the following to the if block:

slideInTransitioningDelegate.disableCompactHeight = true

Build and run the app, bring up a medal count and rotate the device to landscape. The view should now take the entire screen, as shown below:
Landscape

This works great!