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 .pageSheet
dismissal acts like this.)
- Since the iOS 13,
UIViewController
contains a new property calledisModalInPresentation
which must be set totrue
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 calledpresentationControllerShouldDismiss
. - This method is called only if the presented view controller is not dismissed programmatically and its
isModalInPresentation
property is set tofalse
.
Tip: Don't forget to assign
presentationController
's delegate. But be aware, it is known that even just accessing thepresentationController
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:
If your canvas drawing is done with a gesture recognizer, such as your own
UIGestureRecognizer
subclass, enter thebegan
phase before the sheet’s dismiss gesture does. If you recognize as quickly asUIPanGestureRecognizer
, you will win, and the sheet’s dismiss gesture will be subverted.If your canvas drawing is done with a gesture recognizer, setup a dynamic failure requirement with
-shouldBeRequiredToFailByGestureRecognizer:
(or the related delegate method), where you returnNO
if the passed in gesture recognizer is aUIPanGestureRecognizer
.If your canvas drawing is done with manual touch handling (e.g.
touchesBegan:
), override-gestureRecognizerShouldBegin
on your touch handling view, and returnNO
if the passed in gesture recognizer is aUIPanGestureRecognizer
.
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
Close UIdatepicker After Selection When Style Is .Compact
How to Create Generic Convenience Initializer in Swift
How to Create a Circle with Calayer
Swift - How to Deal with Uncaught Exception
Why I Can Not Use Setvalue for Dictionary
Check Os Version Using Swift on MAC Os X
iOS 16 Swiftui List Background
How to Find Realm File Location of a MAC App
Can't Upload .Ipa from Xcode 8, "The Info.Plist Indicates a iOS App, But Submitting a Pkg or Mpkg."
How to Know If a Swiftui Button Is Enabled/Disabled
Get Png Representation of Nsimage in Swift
Swift Error "Domain=Nscocoaerrordomain Code=3840 "Invalid Value Around Character 1."
What Was The Reason for Swift Assignment Evaluation to Void
Nstextfield Non-System-Font Content Clipped When Usessinglelinemode Is True
Naming Convention for Private Properties
Ambiguous Use of 'Filter' When Converting Project to Swift 4
Fbsdksharephoto Not Sharing Link Alongside Photo Using Swift