UIPresentationController Tutorial: Getting Started
Learn how to build custom view controller transitions and presentations with this UIPresentationController tutorial. By Ron Kliffer.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
UIPresentationController Tutorial: Getting Started
30 mins
- Getting Started
- Core Concepts for iOS Transition
- Creating the Transitioning Delegate
- Setting Up the Framework
- Creating the UIPresentationController
- Creating and Initializing a UIPresentationController Subclass
- Setting Up the Dimming View
- Override Presentation Controller Methods
- Implementing the Presentation Styles
- Creating the Animation Controller
- Wiring Up the Animation Controller
- Adaptivity
- Overriding the presented controller
- Where To Go From Here?
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:
-
direction
that tells the animation controller the direction from which it should animate the view controller’s view. -
isPresentation
to tell the animation controller whether to present or dismiss the view controller. - 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:
- 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 thetransitionContext
for the view controller associated with.from
. This is the view controller you’re moving from. - 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. -
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. - 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.
-
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(_:)
ontransitionContext
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.
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?
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:
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 inSlideInPresentationController
. - If no, it returns
.none
, to stay with the implementation ofUIPresentationController
.
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:
This works great!