iOS 13's presentationControllerDidDismiss() Not Called for Popover in Compact Environment


I am updating my app for iOS 13’s new “card-style” modal views. All has been working well using UIAdaptivePresentationControllerDelegate’s presentationControllerDidAttemptToDismiss() and presentationControllerDidDismiss() functions. But, for views that have their .modalPresentationStyle set to .popover, presentationControllerDidDismiss() is not called when being presented in compact environments (such as a phone or iPad in split or slide-over). It’s called correctly when presented in a regular size class environment (such as an iPad full-screen).

My code setting this up is pretty straightforward:

The code presenting the popover:

func showChooser() { // other setup code... navController.modalPresentationStyle = .popover navController.popoverPresentationController?.barButtonItem = self.viewController?.navigationItem.leftBarButtonItem self.present(navController, animated: true) }

Then, the presented controller conforms to UIAdaptivePresentationControllerDelegate and sets up:

// This is in the presented view controller (i.e. the popover) override func viewDidLoad() { // other setup removed for brevity… self.navigationController?.presentationController?.delegate = self } func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { print("did dismiss") self.cancel?() }

When the view is presented in regular size-class environments, it is displayed correctly as a popover. When the user taps outside the popover, then presentationControllerDidDismiss() is called. However, when the same code is presented in a compact environment, it’s displayed correctly (as a card style), but when the user drags down the view, presentationControllerDidDismiss() is not called.

If I change the .modalPresentationStyle to something else such as .pageSheet or .formSheet, then it all works as expected in either compact or regular presentations.

I’ve tried using the delegate's adaptivePresentationStyle() to change the style to .formSheet on compact environments, but presentationControllerDidDismiss() is still not called correctly.

Update: I should have mentioned that my current workaround is to check the size class and change .modalPresentationStyle as needed:

if self.traitCollection.horizontalSizeClass == .compact { navController.modalPresentationStyle = .automatic } else { navController.modalPresentationStyle = .popover navController.popoverPresentationController?.barButtonItem = self.viewController?.navigationItem.leftBarButtonItem }

This works, but it seems that just using the .popover style should adapt properly and call the correct delegate methods.

Update 2: I've updated the code above to clarify that the <em>presented</em> view controller is the one handling the delegate methods.

Also, after digging into this more, I noticed that if the <em>presenting</em> view controller is the delegate and handles the delegate methods, then this all works as expected. Since it also works in the <em>presented</em> view controller for all .modalPresentationStyle's <em>except</em> popover in compact environments, perhaps there is some lifetime issue when popovers are presented in that way?

Any ideas about what I might be doing wrong?


The problem is merely one of timing. You're doing this in the second view controller:

override func viewDidLoad() { self.navigationController?.presentationController?.delegate = self }

That's too <em>late</em>. You can set the delegate where you configure and perform the presentation:

func showChooser() { navController.modalPresentationStyle = .popover navController.popoverPresentationController?.barButtonItem = self.viewController?.navigationItem.leftBarButtonItem navController.presentationController?.delegate = // * navController.viewControllers[0] as! UIAdaptivePresentationControllerDelegate self.present(navController, animated: true) }

If you prefer to insist upon having the second view controller set itself as delegate, do it earlier. The first good opportunity is willMove:

override func willMove(toParent parent: UIViewController?) { self.parent?.presentationController?.delegate = self }

Put navController.presentationController?.delegate = // * navController.viewControllers[0] after self.present(navController, animated: true) otherwise your presentationController might be nil


Thanks heaps for that example - just to extend on the details from Matt and for the benefit of those looking for a generic example (with the creation of a standalone viewController), I believe something like the below should also work:

func presentExampleViewController() { // Any other setup code specific to your app can go here... let exampleViewController = SomeCustomViewController() exampleViewController.presentationController?.delegate = exampleViewController self.present(exampleViewController, animated: true) }