167

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?

Moin Shirazi
  • 4,372
  • 2
  • 26
  • 38
Marcos Tanaka
  • 3,112
  • 3
  • 25
  • 41

13 Answers13

96

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

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 9
    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. – Irina Sep 24 '19 at 19:37
  • 31
    Hi @Irina - 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. – matt Sep 25 '19 at 20:46
  • What delegate protocol do we need to conform to? I have presenting controller as UINavigationControllerDelegate – Sanzio Angeli Sep 28 '19 at 05:32
  • 10
    @matt 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) – Adam Waite Oct 09 '19 at 07:31
  • 5
    @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: https://stackoverflow.com/questions/54602662/unified-uiviewcontroller-became-frontmost-detection – matt Oct 09 '19 at 12:29
  • 1
    presentationControllerDidDismiss(_:). not called when I click back button in Child VC. Any helps? – Krishna Meena Apr 04 '20 at 20:30
  • colorPicker has a didDismiss delegate? –  Oct 09 '20 at 08:44
58

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

musical_coder
  • 3,886
  • 3
  • 15
  • 18
SuddenMoustache
  • 883
  • 7
  • 17
  • 7
    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). – instAustralia Oct 29 '19 at 01:14
  • @instAustralia could you explain why or reference a documentation? Thanks. – Ahmed Osama Dec 21 '19 at 16:11
  • presentationControllerDidDismiss How to get it called when user press back button ? – Krishna Meena Apr 04 '20 at 20:37
  • @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 https://sarunw.com/posts/modality-changes-in-ios13/ – instAustralia Jun 02 '20 at 02:02
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
MobileMon
  • 8,341
  • 5
  • 56
  • 75
Jerland2
  • 1,096
  • 11
  • 28
  • 2
    self.isModalInPresentation = true then drag dismiss is not work. remove that line delegate method is still called okay. thank you. – Yogesh Patel Sep 23 '19 at 10:17
  • 4
    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). – SuddenMoustache Nov 05 '19 at 16:58
  • 1
    Follow up for @Matt 's answer point (a): Using `presentationControllerDidDismiss` should work – gondo Apr 24 '20 at 15:20
  • 1
    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 dismissed – Vitalii Jan 08 '21 at 16:06
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

PiterPan
  • 1,760
  • 2
  • 22
  • 43
  • 2
    Thank you PiterPan. This is working. This is great and fastest solve. – Erkam KUCET Oct 05 '19 at 20:19
  • 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. – Ian Lovejoy Oct 23 '19 at 23:46
  • 21
    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! :) – SuddenMoustache Nov 01 '19 at 16:30
  • 1
    be careful using this for iPad, as iPad defaults to presenting as a pageSheet when presented modally. This will force iPad to present as fullScreen – wyu Dec 06 '19 at 19:06
  • not work for me. I open modal controller. close it with dismiss, but the willAppear not called. Why? thanks – nonickname May 20 '20 at 10:10
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
    }
}
Vitalii
  • 4,267
  • 1
  • 40
  • 45
  • 4
    If your modal view controller is embedded in a navigation controller you might have to call `self.navigationController?.presentationController?.delegate = self` – Oluwatobi Omotayo Aug 27 '21 at 13:30
8

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)
            }
        }
    }
dimohamdy
  • 2,917
  • 30
  • 28
6

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.

craft
  • 2,017
  • 1
  • 21
  • 30
  • What about `self.dismiss(animated: Bool, completion: (() -> Void)?)` – iGhost Jun 08 '20 at 19:05
  • `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. – craft Jun 01 '21 at 17:35
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

coders
  • 2,287
  • 1
  • 12
  • 20
2

in SwiftUI you can use onDismiss closure

func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View
Alirezak
  • 455
  • 5
  • 14
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
}
Kamran Khan
  • 1,367
  • 1
  • 15
  • 19
-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);

-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()
jacob
  • 1,024
  • 9
  • 14
  • 1
    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". – matt Oct 24 '19 at 02:38
  • 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 `viewWillAppear` – jacob Oct 24 '19 at 02:43
  • 1
    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. – matt Oct 24 '19 at 03:07
  • 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. – jacob Oct 24 '19 at 03:23
  • 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. – matt Oct 24 '19 at 03:26
  • I read and tried your sample downloaded from Github, will figure out if it's suitable for my project architect or not. Thanks. – jacob Oct 24 '19 at 03:55
  • Yeah, it's a pain but so is swizzling. Anyway it was fun arguing! :) Thanks for the discussion. – matt Oct 24 '19 at 03:56
-7

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

self.presentingViewController?.viewWillAppear(false)
self.dismiss(animated: true, completion: nil)