How to present UIAlertController when not in a view controller?

前端 未结 30 3116
庸人自扰
庸人自扰 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 07:04

    @agilityvision's answer translated to Swift4/iOS11. I haven't used localized strings, but you can change that easily:

    import UIKit
    
    /** An alert controller that can be called without a view controller.
     Creates a blank view controller and presents itself over that
     **/
    class AlertPlusViewController: UIAlertController {
    
        private var alertWindow: UIWindow?
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            self.alertWindow?.isHidden = true
            alertWindow = nil
        }
    
        func show() {
            self.showAnimated(animated: true)
        }
    
        func showAnimated(animated _: Bool) {
    
            let blankViewController = UIViewController()
            blankViewController.view.backgroundColor = UIColor.clear
    
            let window = UIWindow(frame: UIScreen.main.bounds)
            window.rootViewController = blankViewController
            window.backgroundColor = UIColor.clear
            window.windowLevel = UIWindowLevelAlert + 1
            window.makeKeyAndVisible()
            self.alertWindow = window
    
            blankViewController.present(self, animated: true, completion: nil)
        }
    
        func presentOkayAlertWithTitle(title: String?, message: String?) {
    
            let alertController = AlertPlusViewController(title: title, message: message, preferredStyle: .alert)
            let okayAction = UIAlertAction(title: "Ok", style: .default, handler: nil)
            alertController.addAction(okayAction)
            alertController.show()
        }
    
        func presentOkayAlertWithError(error: NSError?) {
            let title = "Error"
            let message = error?.localizedDescription
            presentOkayAlertWithTitle(title: title, message: message)
        }
    }
    
    0 讨论(0)
  • 2020-11-22 07:06

    I posted a similar question a couple months ago and think I've finally solved the problem. Follow the link at the bottom of my post if you just want to see the code.

    The solution is to use an additional UIWindow.

    When you want to display your UIAlertController:

    1. Make your window the key and visible window (window.makeKeyAndVisible())
    2. Just use a plain UIViewController instance as the rootViewController of the new window. (window.rootViewController = UIViewController())
    3. Present your UIAlertController on your window's rootViewController

    A couple things to note:

    • Your UIWindow must be strongly referenced. If it's not strongly referenced it will never appear (because it is released). I recommend using a property, but I've also had success with an associated object.
    • To ensure that the window appears above everything else (including system UIAlertControllers), I set the windowLevel. (window.windowLevel = UIWindowLevelAlert + 1)

    Lastly, I have a completed implementation if you just want to look at that.

    https://github.com/dbettermann/DBAlertController

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

    Improving on agilityvision's answer, you'll need to create a window with a transparent root view controller and present the alert view from there.

    However as long as you have an action in your alert controller, you don't need to keep a reference to the window. As a final step of the action handler block, you just need to hide the window as part of the cleanup task. By having a reference to the window in the handler block, this creates a temporary circular reference that would be broken once the alert controller is dismissed.

    UIWindow* window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    window.rootViewController = [UIViewController new];
    window.windowLevel = UIWindowLevelAlert + 1;
    
    UIAlertController* alertCtrl = [UIAlertController alertControllerWithTitle:... message:... preferredStyle:UIAlertControllerStyleAlert];
    
    [alertCtrl addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"OK",@"Generic confirm") style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        ... // do your stuff
    
        // very important to hide the window afterwards.
        // this also keeps a reference to the window until the action is invoked.
        window.hidden = YES;
    }]];
    
    [window makeKeyAndVisible];
    [window.rootViewController presentViewController:alertCtrl animated:YES completion:nil];
    
    0 讨论(0)
  • 2020-11-22 07:06

    This works in Swift for normal view controllers and even if there is a navigation controller on the screen:

    let alert = UIAlertController(...)
    
    let alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindowLevelAlert + 1;
    alertWindow.makeKeyAndVisible()
    alertWindow.rootViewController?.presentViewController(alert, animated: true, completion: nil)
    
    0 讨论(0)
  • 2020-11-22 07:06

    iOS13 scene support (when using UIWindowScene)

    import UIKit
    
    private var windows: [String:UIWindow] = [:]
    
    extension UIWindowScene {
        static var focused: UIWindowScene? {
            return UIApplication.shared.connectedScenes
                .first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
        }
    }
    
    class StyledAlertController: UIAlertController {
    
        var wid: String?
    
        func present(animated: Bool, completion: (() -> Void)?) {
    
            //let window = UIWindow(frame: UIScreen.main.bounds)
            guard let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) else {
                return
            }
            window.rootViewController = UIViewController()
            window.windowLevel = .alert + 1
            window.makeKeyAndVisible()
            window.rootViewController!.present(self, animated: animated, completion: completion)
    
            wid = UUID().uuidString
            windows[wid!] = window
        }
    
        open override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
            if let wid = wid {
                windows[wid] = nil
            }
    
        }
    
    }
    
    0 讨论(0)
  • 2020-11-22 07:07

    Create Extension like in Aviel Gross answer. Here You have Objective-C extension.

    Here You have header file *.h

    //  UIAlertController+Showable.h
    
    #import <UIKit/UIKit.h>
    
    @interface UIAlertController (Showable)
    
    - (void)show;
    
    - (void)presentAnimated:(BOOL)animated
                 completion:(void (^)(void))completion;
    
    - (void)presentFromController:(UIViewController *)viewController
                         animated:(BOOL)animated
                       completion:(void (^)(void))completion;
    
    @end
    

    And implementation: *.m

    //  UIAlertController+Showable.m
    
    #import "UIAlertController+Showable.h"
    
    @implementation UIAlertController (Showable)
    
    - (void)show
    {
        [self presentAnimated:YES completion:nil];
    }
    
    - (void)presentAnimated:(BOOL)animated
                 completion:(void (^)(void))completion
    {
        UIViewController *rootVC = [UIApplication sharedApplication].keyWindow.rootViewController;
        if (rootVC != nil) {
            [self presentFromController:rootVC animated:animated completion:completion];
        }
    }
    
    - (void)presentFromController:(UIViewController *)viewController
                         animated:(BOOL)animated
                       completion:(void (^)(void))completion
    {
    
        if ([viewController isKindOfClass:[UINavigationController class]]) {
            UIViewController *visibleVC = ((UINavigationController *)viewController).visibleViewController;
            [self presentFromController:visibleVC animated:animated completion:completion];
        } else if ([viewController isKindOfClass:[UITabBarController class]]) {
            UIViewController *selectedVC = ((UITabBarController *)viewController).selectedViewController;
            [self presentFromController:selectedVC animated:animated completion:completion];
        } else {
            [viewController presentViewController:self animated:animated completion:completion];
        }
    }
    
    @end
    

    You are using this extension in Your implementation file like this:

    #import "UIAlertController+Showable.h"
    
    UIAlertController* alert = [UIAlertController
        alertControllerWithTitle:@"Title here"
                         message:@"Detail message here"
                  preferredStyle:UIAlertControllerStyleAlert];
    
    UIAlertAction* defaultAction = [UIAlertAction
        actionWithTitle:@"OK"
                  style:UIAlertActionStyleDefault
                handler:^(UIAlertAction * action) {}];
    [alert addAction:defaultAction];
    
    // Add more actions if needed
    
    [alert show];
    
    0 讨论(0)
提交回复
热议问题