ChatGPT解决这个技术问题 Extra ChatGPT

Detecting sheet was dismissed on iOS 13

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?

So is there a way to dismiss all modal sheets at one time to the root vc?
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.

m
matt

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


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.
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 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)
@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: stackoverflow.com/questions/54602662/…
presentationControllerDidDismiss(_:). not called when I click back button in Child VC. Any helps?
m
musical_coder

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


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 could you explain why or reference a documentation? Thanks.
presentationControllerDidDismiss How to get it called when user press back button ?
@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-ios13
P
PiterPan

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


Thank you PiterPan. This is working. This is great and fastest solve.
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.
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! :)
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
not work for me. I open modal controller. close it with dismiss, but the willAppear not called. Why? thanks
M
MobileMon

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


self.isModalInPresentation = true then drag dismiss is not work. remove that line delegate method is still called okay. thank you.
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).
Follow up for @Matt 's answer point (a): Using presentationControllerDidDismiss should work
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
d
dimohamdy

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

This only handles dismisses using the slide from the top, not by calling the function dismiss(_).
V
Vitalii

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

If your modal view controller is embedded in a navigation controller you might have to call self.navigationController?.presentationController?.delegate = self
c
craft

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.


What 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.
c
coders

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


is it necessary to cast navigationController.topViewController to presentationViewController? I find it is not
How can I reload data in parent VC after dismissing from Cancel button child VC ?
A
Alirezak

in SwiftUI you can use onDismiss closure

func sheet<Item, Content>(item: Binding<Item?>, onDismiss: (() -> Void)?, content: (Item) -> Content) -> some View

K
Kamran Khan

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
}

A
Abelardo del angel Quiroz

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


Repeats existing answers.
j
jacob

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

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".
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
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.
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.
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.
M
Mikesch8764

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

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

It's not yours to call.