How to present UIAlertController when not in a view controller?

前端 未结 30 3115
庸人自扰
庸人自扰 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:56

    Cross post my answer since these two threads are not flagged as dupes...

    Now that UIViewController is part of the responder chain, you can do something like this:

    if let vc = self.nextResponder()?.targetForAction(#selector(UIViewController.presentViewController(_:animated:completion:)), withSender: self) as? UIViewController {
    
        let alert = UIAlertController(title: "A snappy title", message: "Something bad happened", preferredStyle: .Alert)
        alert.addAction(UIAlertAction(title: "OK", style: .Default, handler: nil))
    
        vc.presentViewController(alert, animated: true, completion: nil)
    }
    
    0 讨论(0)
  • 2020-11-22 06:57

    Pretty generic UIAlertController extension for all cases of UINavigationController and/or UITabBarController. Also works if there's a modal VC on screen at the moment.

    Usage:

    //option 1:
    myAlertController.show()
    //option 2:
    myAlertController.present(animated: true) {
        //completion code...
    }
    

    This is the extension:

    //Uses Swift1.2 syntax with the new if-let
    // so it won't compile on a lower version.
    extension UIAlertController {
    
        func show() {
            present(animated: true, completion: nil)
        }
    
        func present(#animated: Bool, completion: (() -> Void)?) {
            if let rootVC = UIApplication.sharedApplication().keyWindow?.rootViewController {
                presentFromController(rootVC, animated: animated, completion: completion)
            }
        }
    
        private func presentFromController(controller: UIViewController, animated: Bool, completion: (() -> Void)?) {
            if  let navVC = controller as? UINavigationController,
                let visibleVC = navVC.visibleViewController {
                    presentFromController(visibleVC, animated: animated, completion: completion)
            } else {
              if  let tabVC = controller as? UITabBarController,
                  let selectedVC = tabVC.selectedViewController {
                    presentFromController(selectedVC, animated: animated, completion: completion)
              } else {
                  controller.presentViewController(self, animated: animated, completion: completion)
              }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-22 06:57

    In addition to great answers given (agilityvision, adib, malhal). To reach queueing behaviour like in good old UIAlertViews (avoid alert windows overlap), use this block to observe window level availability:

    @interface UIWindow (WLWindowLevel)
    
    + (void)notifyWindowLevelIsAvailable:(UIWindowLevel)level withBlock:(void (^)())block;
    
    @end
    
    @implementation UIWindow (WLWindowLevel)
    
    + (void)notifyWindowLevelIsAvailable:(UIWindowLevel)level withBlock:(void (^)())block {
        UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
        if (keyWindow.windowLevel == level) {
            // window level is occupied, listen for windows to hide
            id observer;
            observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIWindowDidBecomeHiddenNotification object:keyWindow queue:nil usingBlock:^(NSNotification *note) {
                [[NSNotificationCenter defaultCenter] removeObserver:observer];
                [self notifyWindowLevelIsAvailable:level withBlock:block]; // recursive retry
            }];
    
        } else {
            block(); // window level is available
        }
    }
    
    @end
    

    Complete example:

    [UIWindow notifyWindowLevelIsAvailable:UIWindowLevelAlert withBlock:^{
        UIWindow *alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        alertWindow.windowLevel = UIWindowLevelAlert;
        alertWindow.rootViewController = [UIViewController new];
        [alertWindow makeKeyAndVisible];
    
        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Alert" message:nil preferredStyle:UIAlertControllerStyleAlert];
        [alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
            alertWindow.hidden = YES;
        }]];
    
        [alertWindow.rootViewController presentViewController:alertController animated:YES completion:nil];
    }];
    

    This will allow you to avoid alert windows overlap. Same method can be used to separate and put in queue view controllers for any number of window layers.

    0 讨论(0)
  • 2020-11-22 06:58

    Some of these answers only worked partly for me, combining them in the following class method in AppDelegate was the solution for me. It works on iPad, in UITabBarController views, in UINavigationController, en when presenting modals. Tested on iOS 10 and 13.

    + (UIViewController *)rootViewController {
        UIViewController *rootViewController = [UIApplication sharedApplication].delegate.window.rootViewController;
        if([rootViewController isKindOfClass:[UINavigationController class]])
            rootViewController = ((UINavigationController *)rootViewController).viewControllers.firstObject;
        if([rootViewController isKindOfClass:[UITabBarController class]])
            rootViewController = ((UITabBarController *)rootViewController).selectedViewController;
        if (rootViewController.presentedViewController != nil)
            rootViewController = rootViewController.presentedViewController;
        return rootViewController;
    }
    

    Usage:

    [[AppDelegate rootViewController] presentViewController ...
    
    0 讨论(0)
  • 2020-11-22 07:01

    If anyone is interested I created a Swift 3 version of @agilityvision answer. The code:

    import Foundation
    import UIKit
    
    extension UIAlertController {
    
        var window: UIWindow? {
            get {
                return objc_getAssociatedObject(self, "window") as? UIWindow
            }
            set {
                objc_setAssociatedObject(self, "window", newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            }
        }
    
        open override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            self.window?.isHidden = true
            self.window = nil
        }
    
        func show(animated: Bool = true) {
            let window = UIWindow(frame: UIScreen.main.bounds)
            window.rootViewController = UIViewController(nibName: nil, bundle: nil)
    
            let delegate = UIApplication.shared.delegate
            if delegate?.window != nil {
                window.tintColor = delegate!.window!!.tintColor
            }
    
            window.windowLevel = UIApplication.shared.windows.last!.windowLevel + 1
    
            window.makeKeyAndVisible()
            window.rootViewController!.present(self, animated: animated, completion: nil)
    
            self.window = window
        }
    }
    
    0 讨论(0)
  • 2020-11-22 07:03

    You can do the following with Swift 2.2:

    let alertController: UIAlertController = ...
    UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
    

    And Swift 3.0:

    let alertController: UIAlertController = ...
    UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
    
    0 讨论(0)
提交回复
热议问题