How to Use Swipe to Dismiss While Presenting a Fullscreen Modal in iOS 13

Is it possible to use swipe to dismiss while presenting a fullscreen modal in iOS 13?

It seems that the swipe to dismiss will only work if the modal is presented as a sheet, as stated in this year's wwdc:

Now, what do you all have to do to support Pull to Dismiss? In general, nothing. If you present something as a Sheet, the ability to pull it down comes for free.

And it makes sense. When you present it as a sheet, the UI makes it look like you can swipe the modal down. When you present it on fullscreen, it would not be intuitive for the user that he should swipe the page down to dismiss. I'd rather use a button on this case.

Presenting modal in iOS 13 fullscreen

With iOS 13, as stated in the Platforms State of the Union during the WWDC 2019, Apple introduced a new default card presentation. In order to force the fullscreen you have to specify it explicitly with:

let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen //or .overFullScreen for transparency
self.present(vc, animated: true, completion: nil)

iOS 13 Modals - Calling swipe dismissal programmatically

Mojtaba Hosseini, answer is something I was looking for.

Currently, I need to write a delegate function to let the presenting view know that the user dismissed the modal PLUS do the presentationControllerDidDismiss handler for swipe dismissals:

@IBAction func btnDismissTap(_ sender: Any) {
self.dismiss(animated: true, completion: {
self.delegate?.myModalViewDidDismiss()
})
}

I wanted to handle both of these the same way and Mojtaba's answer works for me. However, presentationControllerDidDismiss does not get invoked if you call it inside of the self.dismiss completion block, you need to call it before.

I adapted my code to use "presentationControllerWillDismiss" (for clarity) and simply called the delegate before I dismiss programmatically in my modals and it works great.

@IBAction func btnDismissTap(_ sender: Any) {
if let pvc = self.presentationController {
pvc.delegate?.presentationControllerWillDismiss?(pvc)
}
self.dismiss(animated: true, completion: nil)
}

Now, I no longer need to create delegate functions to handle modal dismissals in code and my swipe handler takes care of all scenarios.

FYI, what I'm "handling" is doing some UI clean up (de-selections, etc) on the presenting UI once the modal is dismissed.

Disable the interactive dismissal of presented view controller

Option 1:

viewController.isModalInPresentation = true

Disabled interactive dismissal

(Disabled interactive .pageSheet dismissal acts like this.)

  • Since the iOS 13, UIViewController contains a new property called isModalInPresentation which must be set to true to prevent the interactive dismissal.
  • It basically ignores events outside the view controller's bounds. Bear that in mind if you are using not only the automatic style but also presentation styles like .popover etc.
  • This property is false by default.

From the official docs: If true, UIKit ignores events outside the view controller's bounds and prevents the interactive dismissal of the view controller while it is onscreen.



Option 2:

func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false
}
  • Since the iOS 13, UIAdaptivePresentationControllerDelegate contains a new method called presentationControllerShouldDismiss.
  • This method is called only if the presented view controller is not dismissed programmatically and its isModalInPresentation property is set to false.

Tip: Don't forget to assign presentationController's delegate. But be aware, it is known that even just accessing the presentationController can cause a memory leak.

Can I have Full Screen ViewController presented, but still able to swipe down, to get back to Parent (launcher) ViewController

You could try the following example:

class ViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
}

//Button is added via storyboard
@IBAction func presentButtonDidTap(_ sender: UIButton) {
let vc = NewViewController()
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
}
}

class NewViewController: UIViewController {

override func viewDidLoad() {
self.view.backgroundColor = .gray

let gesture = UISwipeGestureRecognizer(target: self, action: #selector(dismissVC))
gesture.direction = .down
view.isUserInteractionEnabled = true // For UIImageView
view.addGestureRecognizer(gesture)
}

@objc
private func dismissVC() {
dismiss(animated: true)
}
}

Detecting sheet was dismissed on iOS 13

Is there a way to detect that the presented view controller sheet was dismissed?

Yes.

Some other function I can override in the parent view controller rather than using some sort of delegate?

No. "Some sort of delegate" is how you do it. Make yourself the presentation controller's delegate and override presentationControllerDidDismiss(_:).

https://developer.apple.com/documentation/uikit/uiadaptivepresentationcontrollerdelegate/3229889-presentationcontrollerdiddismiss


The lack of a general runtime-generated event informing you that a presented view controller, whether fullscreen or not, has been dismissed, is indeed troublesome; but it's not a new issue, because there have always been non-fullscreen presented view controllers. It's just that now (in iOS 13) there are more of them! I devote a separate question-and-answer to this topic elsewhere: Unified UIViewController "became frontmost" detection?.

Disable gesture to pull down form/page sheet modal presentation

In general, you shouldn't try to disable the swipe to dismiss functionality, as users expect all form/page sheets to behave the same across all apps. Instead, you may want to consider using a full-screen presentation style. If you do want to use a sheet that can't be dismissed via swipe, set isModalInPresentation = true, but note this still allows the sheet to be pulled down vertically and it'll bounce back up upon releasing the touch. Check out the UIAdaptivePresentationControllerDelegate documentation to react when the user tries to dismiss it via swipe, among other actions.

If you have a scenario where your app's gesture or touch handling is impacted by the swipe to dismiss feature, I did receive some advice from an Apple engineer on how to fix that.

If you can prevent the system's pan gesture recognizer from beginning, this will prevent the gestural dismissal. A few ways to do this:

  1. If your canvas drawing is done with a gesture recognizer, such as your own UIGestureRecognizer subclass, enter the began phase before the sheet’s dismiss gesture does. If you recognize as quickly as UIPanGestureRecognizer, you will win, and the sheet’s dismiss gesture will be subverted.

  2. If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with -shouldBeRequiredToFailByGestureRecognizer: (or the related delegate method), where you return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.

  3. If your canvas drawing is done with manual touch handling (e.g. touchesBegan:), override -gestureRecognizerShouldBegin on your touch handling view, and return NO if the passed in gesture recognizer is a UIPanGestureRecognizer.

With my setup #3 proved to work very well. This allows the user to swipe down anywhere outside of the drawing canvas to dismiss (like the nav bar), while allowing the user to draw without moving the sheet, just as one would expect.

I cannot recommend trying to find the gesture to disable it, as it seems to be rather dynamic and can reenable itself when switching between different size classes for example, and this could change in future releases.

iOS 13 Dismiss Modal Page Sheet with a button

If the originating VC is of type PresentingVC, and the modal of type PresentedVC, I'd use the below approach. Given the segue statement above I assume you're using storyboards, but if not replace the prepare(for segue:) with injecting the delegate value when you instantiate yourPresentedVC

For starters, set your PresentedVC up to hold a delegate by defining the delegate protocol and providing a delegate property.


protocol PresentedVCDelegate {
func presentedVCDidUpdateDatabase()
}

class PresentedVC {
var delegate: PresentedVCDelegate?

@IBAction buttontapped(_ sender: Any) {
//existing code to validate and save data to databse
delegate?. presentedVCDidUpdateDatabase()
dismiss(animated: true)
}
}

Update the PresentingVC so that it injects itself as the delegate when instantiating its child VC:

class PresentingVC {

//all the current code

// and amend preapre(for:) something like
func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// Get the new view controller.
if let presented = segue.destination as? PresentedVC {
presented.delegate = self
//anything else you do already
}
}
}

The extend it to support the protocol method

extension PresentingVC: PresentedVCDelegate {
func presentedVCDidUpdateDatabase() {
tableView.reloadData()
//any other work necessary after PresentedVC exits
}
}

Note: written from memory and not compiled, so may contain minor typos, but hopefully it's enough detail to get the concept across?



Related Topics



Leave a reply



Submit