How to present UIAlertController when not in a view controller?

前端 未结 30 3049
庸人自扰
庸人自扰 2020-11-22 06:21

Scenario: The user taps on a button on a view controller. The view controller is the topmost (obviously) in the navigation stack. The tap invokes a utility class method call

相关标签:
30条回答
  • 2020-11-22 06:54

    For iOS 13, building on the answers by mythicalcoder and bobbyrehm:

    In iOS 13, if you are creating your own window to present the alert in, you are required to hold a strong reference to that window or else your alert won't be displayed because the window will be immediately deallocated when its reference exits scope.

    Furthermore, you'll need to set the reference to nil again after the alert is dismissed in order to remove the window to continue to allow user interaction on the main window below it.

    You can create a UIViewController subclass to encapsulate the window memory management logic:

    class WindowAlertPresentationController: UIViewController {
    
        // MARK: - Properties
    
        private lazy var window: UIWindow? = UIWindow(frame: UIScreen.main.bounds)
        private let alert: UIAlertController
    
        // MARK: - Initialization
    
        init(alert: UIAlertController) {
    
            self.alert = alert
            super.init(nibName: nil, bundle: nil)
        }
    
        required init?(coder aDecoder: NSCoder) {
    
            fatalError("This initializer is not supported")
        }
    
        // MARK: - Presentation
    
        func present(animated: Bool, completion: (() -> Void)?) {
    
            window?.rootViewController = self
            window?.windowLevel = UIWindow.Level.alert + 1
            window?.makeKeyAndVisible()
            present(alert, animated: animated, completion: completion)
        }
    
        // MARK: - Overrides
    
        override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
    
            super.dismiss(animated: flag) {
                self.window = nil
                completion?()
            }
        }
    }
    

    You can use this as is, or if you want a convenience method on your UIAlertController, you can throw it in an extension:

    extension UIAlertController {
    
        func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
    
            let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
            windowAlertPresentationController.present(animated: animated, completion: completion)
        }
    }
    
    0 讨论(0)
  • 2020-11-22 06:54

    You can send the current view or controller as a parameter:

    + (void)myUtilityMethod:(id)controller {
        // do stuff
        // something bad happened, display an alert.
    }
    
    0 讨论(0)
  • 2020-11-22 06:55
    extension UIApplication {
        /// The top most view controller
        static var topMostViewController: UIViewController? {
            return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
        }
    }
    
    extension UIViewController {
        /// The visible view controller from a given view controller
        var visibleViewController: UIViewController? {
            if let navigationController = self as? UINavigationController {
                return navigationController.topViewController?.visibleViewController
            } else if let tabBarController = self as? UITabBarController {
                return tabBarController.selectedViewController?.visibleViewController
            } else if let presentedViewController = presentedViewController {
                return presentedViewController.visibleViewController
            } else {
                return self
            }
        }
    }
    

    With this you can easily present your alert like so

    UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)
    

    One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController will return a UIAlertController. Presenting on top of a UIAlertController has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController) before presenting, or add an else if case to return nil if self is UIAlertController

    extension UIViewController {
        /// The visible view controller from a given view controller
        var visibleViewController: UIViewController? {
            if let navigationController = self as? UINavigationController {
                return navigationController.topViewController?.visibleViewController
            } else if let tabBarController = self as? UITabBarController {
                return tabBarController.selectedViewController?.visibleViewController
            } else if let presentedViewController = presentedViewController {
                return presentedViewController.visibleViewController
            } else if self is UIAlertController {
                return nil
            } else {
                return self
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 06:55

    Swift 5

    It's important to hide the window after showing the message.

    func showErrorMessage(_ message: String) {
        let alertWindow = UIWindow(frame: UIScreen.main.bounds)
        alertWindow.rootViewController = UIViewController()
    
        let alertController = UIAlertController(title: "Error", message: message, preferredStyle: UIAlertController.Style.alert)
        alertController.addAction(UIAlertAction(title: "Close", style: UIAlertAction.Style.cancel, handler: { _ in
            alertWindow.isHidden = true
        }))
        
        alertWindow.windowLevel = UIWindow.Level.alert + 1;
        alertWindow.makeKeyAndVisible()
        alertWindow.rootViewController?.present(alertController, animated: true, completion: nil)
    }
    
    0 讨论(0)
  • 2020-11-22 06:55

    I tried everything mentioned, but with no success. The method which I used for Swift 3.0 :

    extension UIAlertController {
        func show() {
            present(animated: true, completion: nil)
        }
    
        func present(animated: Bool, completion: (() -> Void)?) {
            if var topController = UIApplication.shared.keyWindow?.rootViewController {
                while let presentedViewController = topController.presentedViewController {
                    topController = presentedViewController
                }
                topController.present(self, animated: animated, completion: completion)
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 06:56

    Adding on to Zev's answer (and switching back to Objective-C), you could run into a situation where your root view controller is presenting some other VC via a segue or something else. Calling presentedViewController on the root VC will take care of this:

    [[UIApplication sharedApplication].keyWindow.rootViewController.presentedViewController presentViewController:alertController animated:YES completion:^{}];
    

    This straightened out an issue I had where the root VC had segued to another VC, and instead of presenting the alert controller, a warning like those reported above was issued:

    Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!
    

    I haven't tested it, but this may also be necessary if your root VC happens to be a navigation controller.

    0 讨论(0)
提交回复
热议问题