Marcelo de Aguiar
Marcelo de Aguiar

Reputation: 1442

Why dismissing a UIAlertController calls dismiss on UINavigationController?

I'm trying to understand why when dismissing a UIAlertController from a view controller presented in a navigation calls the dimiss(animetaded:) on the UINavigationController.

The reason is that I have inherited from UINavigationController to add some logic to when the navigation is dismissed but it is unintentionally called every time an alert is dismissed.

In my understanding the presentingViewController is responsible for dismissing a presented controller but it seems that this is not the case here.

What am I missing?

To reproduce, run then code below and it will log the message "DISMISS ON NAVIGATION".

    class RootViewController: UIViewController {
        override func viewDidAppear(_ animated: Bool) {
            super.viewDidAppear(animated)

            let alert = UIAlertController(title: "", message: "", preferredStyle: .actionSheet)
            alert.addAction(UIAlertAction(title: "cancel", style: .cancel, handler: nil))
            present(alert, animated: true)
        }
    }

    class Nav: UINavigationController {
        override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
            print("DISMISS ON NAVIGATION")
            super.dismiss(animated: flag, completion: nil)
        }
    }

Now present the navigation controller from anywhere.

            let contrl = RootViewController()
            contrl.view.backgroundColor = .red
            contrl.definesPresentationContext = true

            let nav = Nav(rootViewController: contrl)

            present(nav, animated: true, completion: nil)

Edit: Updated code to make RootViewController define a presentation context. Edit2: Update code to better represent the scenario.

Upvotes: 0

Views: 1447

Answers (1)

André Slotta
André Slotta

Reputation: 14040

The reason is that the navigation controller is the one who presents the alert controller even when you call present on your view controller. Same goes for the dismiss call.

If you want your view controller to present the alert set its definesPresentationContext property to true.

See https://developer.apple.com/documentation/uikit/uiviewcontroller/1621380-present...

The object on which you call this method may not always be the one that handles the presentation. Each presentation style has different rules governing its behavior. For example, a full-screen presentation must be made by a view controller that itself covers the entire screen. If the current view controller is unable to fulfill a request, it forwards the request up the view controller hierarchy to its nearest parent, which can then handle or forward the request.

... and https://developer.apple.com/documentation/uikit/uiviewcontroller/1621456-definespresentationcontext:

When using the UIModalPresentationStyle.currentContext or UIModalPresentationStyle.overCurrentContext style to present a view controller, this property controls which existing view controller in your view controller hierarchy is actually covered by the new content. When a context-based presentation occurs, UIKit starts at the presenting view controller and walks up the view controller hierarchy. If it finds a view controller whose value for this property is true, it asks that view controller to present the new view controller. If no view controller defines the presentation context, UIKit asks the window’s root view controller to handle the presentation. The default value for this property is false. Some system-provided view controllers, such as UINavigationController, change the default value to true.

Update:

For your specific problem (if I understood correctly) maybe it's the best solution to keep the presentation logic as is (navigation controller presents) but instead add a check in the navigation controller's dismiss method:

override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    if !(presentedViewController is UIAlertController) {
        // your additional logic
    }
    super.dismiss(animated: flag, completion: completion)
}

Upvotes: 2

Related Questions