Apple introduced popover presentation controllers that adapt to size classes in iOS 8. So why I am only writing about them now? Well partly because I only just got around to it but mostly as they still seem to cause problems. The popover presentation controller delegate methods are for me some of the most confusing methods in UIKit. So here is my quick and simple introduction to making a popover adapt to size classes.
Last updated: Jun 12, 2020
Presenting a Popover
Let’s get the basic setup out of the way quickly as it’s not so interesting. Here’s the storyboard for our App:
The two buttons both trigger a segue of type Present As Popover. The first shows a plain view controller, the second shows a table view controller embedded in a navigation controller to allow a further segue to a detail view controller. Here’s how the two popovers look when running fullscreen on an iPad:
I set the popover presentation controller delegate and popover anchor point in the prepare(for:sender:)
function of the root view controller:
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
switch segue.identifier {
case "SimpleSegue"?:
let simplePPC = segue.destination.popoverPresentationController
simplePPC?.delegate = self
simplePPC?.sourceView = simpleButton
simplePPC?.sourceRect = simpleButton.bounds
At this point the popovers work and will even by default adapt to full screen modal presentations for compact size classes. In iOS 13, the card style modal presentation can be dismissed with a drag gesture. If we’re supporting iOS 12 or earlier we’ll need to add a button to dismiss the popover when we are modal. Let’s fix that first.
The UIPopoverPresentationController Delegate
To add a dismiss button when presenting our popover full screen we need to implement the delegate. Let’s do that in an extension to our view controller class:
extension RootViewController: UIPopoverPresentationControllerDelegate {
}
Adding the button
Our popover needs a done button when we present it in a modal full screen style. I’ll add the button to the navigation bar of a navigation controller. If our presented view controller is not already embedded in a navigation controller I’ll create one.
The delegate function we need is presentationController:viewControllerForAdaptivePresentationStyle:
which is inherited from the UIAdaptivePresentationControllerDelegate
protocol. This is where I find the documentation to be confusing. We need to return the view controller for when the popover is shown in a compact width:
func presentationController(_ controller: UIPresentationController,
viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle)
-> UIViewController? {
We first check if we have a modal presentation style. If not, we just return the current presented view controller unchanged:
if style == .none {
return controller.presentedViewController
}
I then check if the presented view controller is a navigation controller. It it’s not I create one to wrap the presented view controller:
// Find or create the navigation controller
let navigationController: UINavigationController = {
guard let navigationController = controller.presentedViewController
as? UINavigationController else {
return UINavigationController(rootViewController: controller.presentedViewController)
}
return navigationController
}()
Once we have a navigation controller we can add the dismiss button to it and return it:
addDismissButton(navigationController)
return navigationController
}
I have a short helper function to add the dismiss button to the navigation controller:
private func addDismissButton(_ navigationController: UINavigationController) {
let rootViewController = navigationController.viewControllers[0]
rootViewController.navigationItem.leftBarButtonItem =
UIBarButtonItem.init(barButtonSystemItem: .Done,
target: self, action: #selector(didDismissPresentedView))
}
Note that we add the button to the root view controller which is navigationController.viewControllers[0]
and not navigationController.topViewController
which might be the detail view controller when the transition happens.
We also need the function that dismisses the presented view controller when the user taps the done button:
@objc private func didDismissPresentedView() {
presentedViewController?.dismiss(animated: true, completion: nil)
}
At this point we have a popover that adapts to a full screen modal display when the view transitions to a compact size class. It also has a done button we can use to dismiss it which is good:
Removing the button
The final step which often seems to be missed is to remove the button if we transition back to a regular size class. Otherwise you end up with something like this in the navigation bar:
The best place to remove the button that I can find is a method that was only introduced in iOS 8.3. If we are about to present with a non modal style we check for a navigation controller and if present remove the done button:
func presentationController(_ presentationController: UIPresentationController,
willPresentWithAdaptiveStyle style: UIModalPresentationStyle,
transitionCoordinator: UIViewControllerTransitionCoordinator?) {
if style == .none,
let navController = presentationController.presentedViewController as? UINavigationController {
removeDismissButton(navController)
}
}
The removeDismissButton function is a reverse of the addDismissButton:
private func removeDismissButton(_ navigationController: UINavigationController) {
let rootViewController = navigationController.viewControllers[0]
rootViewController.navigationItem.leftBarButtonItem = nil;
}
Further Reading
The UIPopoverPresentationControllerDelegate
and UIAdaptivePresentationControllerDelegate
documentation cover more ways to interact with and change the presentation. The full project code is in my GitHub repository at the link below. You may also want to check out the WWDC 2014 session:
- AdaptivePopover sample code
- WWDC 2014 A Look Inside Presentation Controllers
- Presentation Controllers and Adaptive Presentations A great summary from Douglas Hill on the problems the PSPDFKit team faced with presentation controllers.