Before iOS 13, presented view controllers used to cover the entire screen. And, when dismissed, the parent view controller viewDidAppear
function were executed.
Now iOS 13 will present view controllers as a sheet as default, which means the card will partially cover the underlying view controller, which means that viewDidAppear
will not be called, because the parent view controller has never actually disappeared.
Is there a way to detect that the presented view controller sheet was dismissed? Some other function I can override in the parent view controller rather than using some sort of delegate?
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(_:)
.
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?.
Here's a code example of a parent view-controller which is notified when the child view-controller it presents as a sheet (i.e., in the default iOS 13 manner) is dismissed:
public final class Parent: UIViewController, UIAdaptivePresentationControllerDelegate
{
// This is assuming that the segue is a storyboard segue;
// if you're manually presenting, just set the delegate there.
public override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
if segue.identifier == "mySegue" {
segue.destination.presentationController?.delegate = self;
}
}
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
// Only called when the sheet is dismissed by DRAGGING.
// You'll need something extra if you call .dismiss() on the child.
// (I found that overriding dismiss in the child and calling
// presentationController.delegate?.presentationControllerDidDismiss
// works well).
}
}
Jerland2's answer is confused, since (a) the original questioner wanted to get a function call when the sheet is dismissed (whereas he implemented presentationControllerDidAttemptToDismiss, which is called when the user tries and fails to dismiss the sheet), and (b) setting isModalInPresentation is entirely orthogonal and in fact will make the presented sheet undismissable (which is the opposite of what OP wants).
Another option to get back viewWillAppear
and viewDidAppear
is set
let vc = UIViewController()
vc.modalPresentationStyle = .fullScreen
this option cover full screen and after dismiss, calls above methods
For future readers here is a more complete answer with implementation:
In the root view controllers prepare for segue add the following (Assuming your modal has a nav controller)
// Modal Dismiss iOS 13
modalNavController.presentationController?.delegate = modalVc
In the modal view controller add the following delegate + method
// MARK: - iOS 13 Modal (Swipe to Dismiss)
extension ModalViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
print("slide to dismiss stopped")
self.dismiss(animated: true, completion: nil)
}
}
Ensure in the modal View Controller that the following property is true in order for the delegate method to be called
self.isModalInPresentation = true
Profit
presentationControllerDidDismiss
should work
presentationControllerDidAttemptToDismiss
is intended for cases when user tried to dismiss but was prevented programmatically (read the doc for that method carefully). The presentationControllerWillDismiss
method is the one to detect user's intention to dismiss OR presentationControllerShouldDismiss
to control dismissing OR presentationControllerDidDismiss
to detect the fact of being dismissed
Swift
General Solution to call viewWillAppear
in iOS13
class ViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
print("viewWillAppear")
}
//Show new viewController
@IBAction func show(_ sender: Any) {
let newViewController = NewViewController()
//set delegate of UIAdaptivePresentationControllerDelegate to self
newViewController.presentationController?.delegate = self
present(newViewController, animated: true, completion: nil)
}
}
extension UIViewController: UIAdaptivePresentationControllerDelegate {
public func presentationControllerDidDismiss( _ presentationController: UIPresentationController) {
if #available(iOS 13, *) {
//Call viewWillAppear only in iOS 13
viewWillAppear(true)
}
}
}
dismiss(_)
.
If you want to do something when user closes the modal sheet from within that sheet. Let's assume you already have some Close button with an @IBAction
and a logic to show an alert before closing or do something else. You just want to detect the moment when user makes push down on such a controller.
Here's how:
class MyModalSheetViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.presentationController?.delegate = self
}
@IBAction func closeAction(_ sender: Any) {
// your logic to decide to close or not, when to close, etc.
}
}
extension MyModalSheetViewController: UIAdaptivePresentationControllerDelegate {
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
return false // <-prevents the modal sheet from being closed
}
func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
closeAction(self) // <- called after the modal sheet was prevented from being closed and leads to your own logic
}
}
self.navigationController?.presentationController?.delegate = self
Override viewWillDisappear
on the UIViewController
that's being dismissed. It will alert you to a dismissal via isBeingDismissed
boolean flag.
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if isBeingDismissed {
print("user is dismissing the vc")
}
}
** If the user is halfway through the swipe down and swipes the card back up, it'll still register as being dismissed, even if the card is not dismissed. But that's an edge case you may not care about.
self.dismiss(animated: Bool, completion: (() -> Void)?)
self.dismiss(animated: Bool, completion: (() -> Void)?)
won't detect the dismissal. Instead it would cause an action to happen and then you're piggybacking on it to do some work. Using viewWillDisappear
will listen for the event of dismissal.
DRAG OR CALL DISMISS FUNC will work with below code.
1) In root view controller, you tell that which is its presentation view controller as below code
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "presenterID" {
let navigationController = segue.destination as! UINavigationController
if #available(iOS 13.0, *) {
let controller = navigationController.topViewController as! presentationviewcontroller
// Modal Dismiss iOS 13
controller.presentationController?.delegate = self
} else {
// Fallback on earlier versions
}
navigationController.presentationController?.delegate = self
}
}
2) Again in the root view controller, you tell what you will do when its presentation view controller is dissmised
public func presentationControllerDidDismiss(
_ presentationController: UIPresentationController)
{
print("presentationControllerDidDismiss")
}
1) In the presentation view controller, When you hit cancel or save button in this picture. Below code will be called.The
self.dismiss(animated: true) {
self.presentationController?.delegate?.presentationControllerDidDismiss?(self.presentationController!)
}
https://i.stack.imgur.com/Arj7f.png
in SwiftUI you can use onDismiss closure
func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View
If someone doesn't have access to the presented view controller, they can just override the following method in presenting view controller and change the modalPresentationStyle
to fullScreen
or can add one of the strategies mentioned above with this approach
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
if let _ = viewControllerToPresent as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
super.present(viewControllerToPresent, animated: flag, completion: completion)
}
if presented view controller is navigation controller and you want to check the root controller, can change the above condition to be like
if let _ = (viewControllerToPresent as? UINavigationController)?.viewControllers.first as? TargetVC {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
If you used the ModalPresentationStyle in FullScreen, the behavior of the controller is back as usual.
ConsultarController controllerConsultar = this.Storyboard.InstantiateViewController("ConsultarController") as ConsultarController; controllerConsultar.ModalPresentationStyle = UIModalPresentationStyle.FullScreen; this.NavigationController.PushViewController(controllerConsultar, true);
From my point of view, Apple should not set pageSheet
is the default modalPresentationStyle
I'd like to bring fullScreen
style back to default by using swizzling
Like this:
private func _swizzling(forClass: AnyClass, originalSelector: Selector, swizzledSelector: Selector) {
if let originalMethod = class_getInstanceMethod(forClass, originalSelector),
let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector) {
method_exchangeImplementations(originalMethod, swizzledMethod)
}
}
extension UIViewController {
static func preventPageSheetPresentationStyle () {
UIViewController.preventPageSheetPresentation
}
static let preventPageSheetPresentation: Void = {
if #available(iOS 13, *) {
_swizzling(forClass: UIViewController.self,
originalSelector: #selector(present(_: animated: completion:)),
swizzledSelector: #selector(_swizzledPresent(_: animated: completion:)))
}
}()
@available(iOS 13.0, *)
private func _swizzledPresent(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil) {
if viewControllerToPresent.modalPresentationStyle == .pageSheet
|| viewControllerToPresent.modalPresentationStyle == .automatic {
viewControllerToPresent.modalPresentationStyle = .fullScreen
}
_swizzledPresent(viewControllerToPresent, animated: flag, completion: completion)
}
}
And then put this line to your AppDelegate
UIViewController.preventPageSheetPresentationStyle()
presentingViewController
will not trigger viewWillAppear
viewWillAppear
was in a sense always wrong. Of course I don't like Apple coming along and cutting the floor out from under me. But as I say, we just have to live with that and do things a new way.
presentedController
) is presented and neither know what is exactly the presentingViewController
. For example: in some cases i have to use UIViewController.topMostViewController()
which returns me the top most view controller on the current window. So that why i would like to do the swizzling to keep current behavior to do right things (refresh data, UI) in viewWillAppear
of my view controllers. If you have any ideas on resolving that, please help.
wouldn't it be simple to call the presentingViewController.viewWillAppear? befor dismissing?
self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)
Success story sharing