Detecting sheet was dismissed on iOS 13
Asked Answered
C

13

170

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?

Cyan answered 12/6, 2019 at 19:15 Comment(4)
Well discussed in developer.apple.com/videos/play/wwdc2019/224Hodden
So is there a way to dismiss all modal sheets at one time to the root vc?Hutner
#56436010Iberian
Why do you need to know when it was dismissed? If it is to reload data and update the UI, Notifications or KVO might be a good alternative.Actor
H
97

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?.

Hodden answered 12/6, 2019 at 19:21 Comment(7)
this is not enough. If you have a nabber in your presented VC and a custom bar button that dismisses your view programmatically, presentation controller did dismiss doesn't get called.Coldiron
Hi @Coldiron - if you dismiss your view programmatically, you don't need a callback because you dismissed your view programmatically — you know you did it because you did it. The delegate method is only in case the user does it.Hodden
What delegate protocol do we need to conform to? I have presenting controller as UINavigationControllerDelegateSubdivide
@Hodden Thanks for the answer. When the view is dismissed programmatically this doesn't get called (as Irina says), and you're right that we know we did it. I just think there's an unnecessary amount of boilerplate code to write just to get a kind of 'viewWillAppear' with the new modal presentation style in iOS13. It gets particularly messy when you're managing routing through an architecture where routing is extracted (in MVVM + coordinators, or a router type in VIPER for example)Coretta
@AdamWaite I agree but this problem isn't new. We've had this problem for years, with popovers, with non-fullscreen presented view controllers, with alerts, and so forth. I regard this as a serious flaw in Apple's repertory of "events". I'm just saying what the reality is and why. I grapple directly with the issue here: #54603162Hodden
presentationControllerDidDismiss(_:). not called when I click back button in Child VC. Any helps?Savino
colorPicker has a didDismiss delegate?Bioastronautics
T
61

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).

Tarsia answered 5/9, 2019 at 10:49 Comment(4)
This works well. Just a tip that if you use a nav controller on your called VC, you should assign the nav controller as the presentationController?,delegate (not the VC the nav has as topViewController).Neukam
@Neukam could you explain why or reference a documentation? Thanks.Mechellemechlin
presentationControllerDidDismiss How to get it called when user press back button ?Savino
@AhmedOsama - the navigation controller is the presentation controller and therefore is the delegate as it will be the one to respond to the dismissal. I did try the VC that is embedded in the Nav Controller too but this is where my actual buttons to dismiss exist and respond. I can't find it directly in Apple docs but it is referenced here sarunw.com/posts/modality-changes-in-ios13Neukam
I
31

For future readers here is a more complete answer with implementation:

  1. 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
  1. 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)
    }
}
  1. Ensure in the modal View Controller that the following property is true in order for the delegate method to be called
    self.isModalInPresentation = true
  1. Profit
Imperturbation answered 28/8, 2019 at 17:55 Comment(4)
self.isModalInPresentation = true then drag dismiss is not work. remove that line delegate method is still called okay. thank you.Racer
This is confused since (a) the original questioner wanted to get a function call when the sheet is dismissed (whereas you've 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).Tarsia
Follow up for @Matt 's answer point (a): Using presentationControllerDidDismiss should workEisenhower
Not quite correct, because 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 dismissedGabar
B
31

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

Brader answered 25/9, 2019 at 7:17 Comment(5)
Thank you PiterPan. This is working. This is great and fastest solve.Delogu
Thank you for this fast and reliable way to restore the former default behavior. It's great to be able to put this fix in place instantly and then plan out a transition to the new behavior in a rational way.Tolerant
This is a workaround rather than a fix. It's not great for everyone to just revert back to iOS 12 style sheets. The iOS 13 ones are cool! :)Tarsia
be careful using this for iPad, as iPad defaults to presenting as a pageSheet when presented modally. This will force iPad to present as fullScreenBreena
not work for me. I open modal controller. close it with dismiss, but the willAppear not called. Why? thanksGiff
G
10

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
    }
}
Gabar answered 8/1, 2021 at 16:42 Comment(1)
If your modal view controller is embedded in a navigation controller you might have to call self.navigationController?.presentationController?.delegate = selfAmorette
M
7

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.

Millstream answered 30/3, 2020 at 23:56 Comment(2)
What about self.dismiss(animated: Bool, completion: (() -> Void)?)Sequestration
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.Millstream
C
7

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)
            }
        }
    }
Chevalier answered 9/4, 2020 at 18:47 Comment(1)
This only handles dismisses using the slide from the top, not by calling the function dismiss(_).Viehmann
S
4

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!)
    }

enter image description here

Stacked answered 2/2, 2020 at 13:26 Comment(2)
is it necessary to cast navigationController.topViewController to presentationViewController? I find it is notSchoen
How can I reload data in parent VC after dismissing from Cancel button child VC ?Savino
A
2

in SwiftUI you can use onDismiss closure

func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View
Abb answered 2/5, 2021 at 10:43 Comment(0)
E
1

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
}
Evoke answered 17/7, 2020 at 9:9 Comment(0)
M
-2

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);

Mukerji answered 21/4, 2020 at 1:18 Comment(1)
Repeats existing answers.Hodden
P
-3

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()
Premarital answered 24/10, 2019 at 2:21 Comment(7)
This is ingenious but I can't agree with it. It's hacky and, more to the point, it goes against the grain of iOS 13. You are supposed to use "card" presentations in iOS 13. The response Apple expects from us is not "work around it"; it's "get over it".Hodden
Agree with your point, this solution doesnt not help to use card presentation style as what Apple encourages us. However, setting it as the default style will make the existing lines of code mistake somewhere because presentingViewController will not trigger viewWillAppearPremarital
Yes, but as I've already said in my own answer, that was always an issue for nonfullscreen presentations (such as popovers and page/form sheet on iPad), so this is nothing new. It's just that now there's more of it. Relying on 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.Hodden
In my project, there're some scenarios that i dont know where a view controller (called 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.Premarital
Well, the solution that I link to at the end of my answer does work to solve that, I believe. It takes some work to configure at presentation time, but basically it guarantees that every presenter (including a presenter of alerts) hears when the presented view controller is dismissed.Hodden
I read and tried your sample downloaded from Github, will figure out if it's suitable for my project architect or not. Thanks.Premarital
Yeah, it's a pain but so is swizzling. Anyway it was fun arguing! :) Thanks for the discussion.Hodden
C
-7

wouldn't it be simple to call the presentingViewController.viewWillAppear? befor dismissing?

self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)
Candlemas answered 23/10, 2019 at 15:56 Comment(1)
It's not yours to call.Hodden

© 2022 - 2024 — McMap. All rights reserved.