【问题标题】:How to present UIAlertController when not in a view controller?不在视图控制器中时如何呈现 UIAlertController?
【发布时间】:2014-12-20 16:52:47
【问题描述】:

场景:用户点击视图控制器上的按钮。视图控制器是导航堆栈中的最顶层(显然)。点击调用在另一个类上调用的实用程序类方法。那里发生了一件坏事,我想在控制返回视图控制器之前在那里显示一个警报。

+ (void)myUtilityMethod {
    // do stuff
    // something bad happened, display an alert.
}

UIAlertView 可以做到这一点(但可能不太合适)。

在这种情况下,您如何在myUtilityMethod 中呈现UIAlertController

【问题讨论】:

    标签: ios uialertcontroller


    【解决方案1】:

    在 WWDC,我在其中一个实验室停下来,问了一位 Apple 工程师同样的问题:“显示 UIAlertController 的最佳做法是什么?”他说他们经常收到这个问题,我们开玩笑说他们应该就这个问题进行一次会议。他说,苹果内部正在创建一个UIWindow 和一个透明的UIViewController,然后在上面显示UIAlertController。基本上是迪伦·贝特曼的回答。

    但我不想使用UIAlertController 的子类,因为这需要我在整个应用程序中更改代码。所以在关联对象的帮助下,我在 UIAlertController 上创建了一个类别,它在 Objective-C 中提供了一个 show 方法。

    以下是相关代码:

    #import "UIAlertController+Window.h"
    #import <objc/runtime.h>
    
    @interface UIAlertController (Window)
    
    - (void)show;
    - (void)show:(BOOL)animated;
    
    @end
    
    @interface UIAlertController (Private)
    
    @property (nonatomic, strong) UIWindow *alertWindow;
    
    @end
    
    @implementation UIAlertController (Private)
    
    @dynamic alertWindow;
    
    - (void)setAlertWindow:(UIWindow *)alertWindow {
        objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    - (UIWindow *)alertWindow {
        return objc_getAssociatedObject(self, @selector(alertWindow));
    }
    
    @end
    
    @implementation UIAlertController (Window)
    
    - (void)show {
        [self show:YES];
    }
    
    - (void)show:(BOOL)animated {
        self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
        self.alertWindow.rootViewController = [[UIViewController alloc] init];
    
        id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
        // Applications that does not load with UIMainStoryboardFile might not have a window property:
        if ([delegate respondsToSelector:@selector(window)]) {
            // we inherit the main window's tintColor
            self.alertWindow.tintColor = delegate.window.tintColor;
        }
    
        // window level is above the top window (this makes the alert, if it's a sheet, show over the keyboard)
        UIWindow *topWindow = [UIApplication sharedApplication].windows.lastObject;
        self.alertWindow.windowLevel = topWindow.windowLevel + 1;
    
        [self.alertWindow makeKeyAndVisible];
        [self.alertWindow.rootViewController presentViewController:self animated:animated completion:nil];
    }
    
    - (void)viewDidDisappear:(BOOL)animated {
        [super viewDidDisappear:animated];
        
        // precaution to ensure window gets destroyed
        self.alertWindow.hidden = YES;
        self.alertWindow = nil;
    }
    
    @end
    

    这是一个示例用法:

    // need local variable for TextField to prevent retain cycle of Alert otherwise UIWindow
    // would not disappear after the Alert was dismissed
    __block UITextField *localTextField;
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Global Alert" message:@"Enter some text" preferredStyle:UIAlertControllerStyleAlert];
    [alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
        NSLog(@"do something with text:%@", localTextField.text);
    // do NOT use alert.textfields or otherwise reference the alert in the block. Will cause retain cycle
    }]];
    [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
        localTextField = textField;
    }];
    [alert show];
    

    UIAlertController 被释放时,创建的UIWindow 将被销毁,因为它是唯一保留UIWindow 的对象。但是,如果您将UIAlertController 分配给属性或通过访问其中一个操作块中的警报来增加其保留计数,UIWindow 将留在屏幕上,锁定您的 UI。避免在需要访问UITextField的情况下查看上面的示例使用代码。

    我用一个测试项目创建了一个 GitHub 存储库:FFGlobalAlertController

    【讨论】:

    • 好东西!只是一些背景知识——我使用子类而不是关联对象,因为我使用的是 Swift。关联对象是 Objective-C 运行时的一个特性,我不想依赖它。 Swift 可能还需要几年的时间才能获得它自己的运行时,但仍然如此。 :)
    • 我真的很喜欢你回答的优雅,但是我很好奇你是如何淘汰新窗口并让原来的窗口再次成为关键的(诚然,我不太喜欢用窗口)。
    • 关键窗口是最上面的可见窗口,所以我的理解是如果您删除/隐藏“关键”窗口,下一个可见窗口将变为“关键”。
    • 在一个类别上实施viewDidDisappear: 看起来是个坏主意。本质上,您是在与框架的viewDidDisappear: 实现竞争。现在可能没问题,但是如果 Apple 决定在未来实现该方法,那么您将无法调用它(即没有类似的 super 指向一个方法的主要实现类别实现)。
    • 效果很好,但是如何在没有额外子类的情况下处理prefersStatusBarHiddenpreferredStatusBarStyle
    【解决方案2】:

    斯威夫特

    let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
    //...
    var rootViewController = UIApplication.shared.keyWindow?.rootViewController
    if let navigationController = rootViewController as? UINavigationController {
        rootViewController = navigationController.viewControllers.first
    }
    if let tabBarController = rootViewController as? UITabBarController {
        rootViewController = tabBarController.selectedViewController
    }
    //...
    rootViewController?.present(alertController, animated: true, completion: nil)
    

    Objective-C

    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];
    //...
    id 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;
    }
    //...
    [rootViewController presentViewController:alertController animated:YES completion:nil];
    

    【讨论】:

    • +1 这是一个非常简单的解决方案。 (我遇到的问题:在 Master/Detail 模板的 DetailViewController 中显示警报 - 在 iPad 上显示,从不在 iPhone 上显示)
    • 很好,您可能想添加另一部分: if (rootViewController.presentedViewController != nil) { rootViewController = rootViewController.presentedViewController; }
    • Swift 3: 'Alert' 已重命名为 'alert': let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
    • 使用委托代替!
    • Swift 5.2:Xcode 现在说 UIApplication.shared.keyWindow 自 iOS 13.0 起已被弃用
    【解决方案3】:

    您可以使用 Swift 2.2 执行以下操作:

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

    还有 Swift 3.0:

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

    【讨论】:

    • 糟糕,我在检查之前接受了。该代码返回根视图控制器,在我的例子中是导航控制器。它不会导致错误,但不会显示警报。
    • 我在控制台中注意到:Warning: Attempt to present &lt;UIAlertController: 0x145bfa30&gt; on &lt;UINavigationController: 0x1458e450&gt; whose view is not in the window hierarchy!.
    • @MurraySagal 有一个导航控制器,您可以随时获取visibleViewController 属性以查看从哪个控制器显示警报。查看the docs
    • 我这样做是因为我不想拿别人的功劳。这是我为 swift 3.0 修改的 @ZevEisenberg 的解决方案。如果我会添加另一个答案,那么我可能会得到他应得的投票。
    • 哦,嘿,我昨天错过了所有的戏剧,但我碰巧刚刚更新了 Swift 3 的帖子。我不知道 SO 对更新新语言版本的旧答案的政策是什么,但是我个人不介意,只要答案是正确的!
    【解决方案4】:

    几个月前我发布了similar question,并认为我终于解决了这个问题。如果您只想查看代码,请点击我帖子底部的链接。

    解决方案是使用额外的 UIWindow。

    当你想显示你的 UIAlertController 时:

    1. 让您的窗口成为关键和可见的窗口 (window.makeKeyAndVisible())
    2. 只需使用一个普通的 UIViewController 实例作为新窗口的 rootViewController。 (window.rootViewController = UIViewController())
    3. 在窗口的 rootViewController 上显示您的 UIAlertController

    需要注意的几点:

    • 您的 UIWindow 必须被强引用。如果它没有被强烈引用,它将永远不会出现(因为它已被释放)。我建议使用属性,但我也成功使用了 associated object
    • 为确保窗口出现在其他所有内容之上(包括系统 UIAlertControllers),我设置了 windowLevel。 (window.windowLevel = UIWindowLevelAlert + 1)

    最后,如果你只是想看看,我有一个完整的实现。

    https://github.com/dbettermann/DBAlertController

    【讨论】:

    • Objective-C 没有这个,对吧?
    • 是的,它甚至可以在 Swift 2.0/iOS 9 中使用。我现在正在开发一个 Objective-C 版本,因为其他人要求它(也许是你)。完成后我会回帖。
    【解决方案5】:

    对于UINavigationController 和/或UITabBarController 的所有情况,非常通用的UIAlertController extension。如果此时屏幕上有模态 VC,也可以使用。

    用法:

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

    这是扩展名:

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

    【讨论】:

    • 我正在使用这个解决方案,我发现它非常完美、优雅、干净......但是,最近我不得不将我的根视图控制器更改为不在视图层次结构中的视图,所以这段代码变得无用。有人想用一个 dix 来继续使用它吗?
    • 我将此解决方案与其他一些方法结合使用:我有一个单例 UI 类,它包含一个(弱!)currentVC 类型为 UIViewController。我有 BaseViewController 继承从UIViewController 并在viewDidAppear 上将UI.currentVC 设置为self,然后在viewWillDisappear 上设置nil。我在应用程序中的所有视图控制器都继承BaseViewController。这样,如果您在 UI.currentVC 中有东西(不是 nil...) - 它肯定不在演示动画的中间,您可以要求它展示您的 UIAlertController
    • 如下所示,根视图控制器可能会显示带有 segue 的内容,在这种情况下,您的最后一个 if 语句失败,所以我必须添加 else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
    【解决方案6】:

    agilityvision's answer 的基础上进行改进,您需要创建一个带有透明根视图控制器的窗口,并从那里显示警报视图。

    但是只要您在警报控制器中有操作,您就不需要保留对窗口的引用。作为动作处理程序块的最后一步,您只需要隐藏窗口作为清理任务的一部分。通过在处理程序块中引用窗口,这会创建一个临时循环引用,一旦解除警报控制器,该引用就会被破坏。

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

    【讨论】:

    • 完美,正是我需要关闭窗口的提示,谢谢队友
    【解决方案7】:

    以下解决方案没有工作,尽管它在所有版本中看起来都非常有希望。 This solution is generating WARNING.

    警告:尝试展示不在窗口层次结构中的视图!

    https://stackoverflow.com/a/34487871/2369867 => 这看起来很有希望。但在Swift 3不是。 所以我在 Swift 3 中回答这个问题,这是 not 模板示例。

    一旦粘贴到任何函数中,这本身就是功能齐全的代码。

    快速Swift 3自包含代码

    let alertController = UIAlertController(title: "<your title>", message: "<your message>", preferredStyle: UIAlertControllerStyle.alert)
    alertController.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.cancel, handler: nil))
    
    let alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindowLevelAlert + 1;
    alertWindow.makeKeyAndVisible()
    alertWindow.rootViewController?.present(alertController, animated: true, completion: nil)
    

    这是在 Swift 3 中经过测试和工作的代码。

    【讨论】:

    • 在加载任何根视图控制器之前,在 App Delegate 中因迁移问题而触发 UIAlertController 的上下文中,这段代码对我来说非常有效。效果很好,没有警告。
    • 提醒一下:您需要存储对UIWindow 的强引用,否则窗口将被释放并在超出范围后很快消失。
    【解决方案8】:

    这是 mythicalcoder's answer 作为扩展,在 Swift 4 中经过测试和工作:

    extension UIAlertController {
    
        func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
            let alertWindow = UIWindow(frame: UIScreen.main.bounds)
            alertWindow.rootViewController = UIViewController()
            alertWindow.windowLevel = UIWindowLevelAlert + 1
            alertWindow.makeKeyAndVisible()
            alertWindow.rootViewController?.present(self, animated: animated, completion: completion)
        }
    
    }
    

    示例用法:

    let alertController = UIAlertController(title: "<Alert Title>", message: "<Alert Message>", preferredStyle: .alert)
    alertController.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil))
    alertController.presentInOwnWindow(animated: true, completion: {
        print("completed")
    })
    

    【讨论】:

    • 这个即使sharedApplication不可访问也可以使用!
    • 我收到错误:对 <..>
    【解决方案9】:

    这在 Swift 中适用于普通视图控制器,即使屏幕上有导航控制器:

    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)
    

    【讨论】:

    • 当我解除警报时,UIWindow 没有响应。可能与windowLevel 有关。我怎样才能让它响应?
    • 听起来新窗口没有被关闭。
    • 看起来窗口没有从顶部移除,所以需要在完成后移除窗口。
    • 完成后将alertWindow 设置为nil
    【解决方案10】:

    添加到 Zev 的答案(并切换回 Objective-C),您可能会遇到根视图控制器通过 segue 或其他方式呈现其他 VC 的情况。在根 VC 上调用 presentViewController 会处理这个问题:

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

    这解决了我遇到的一个问题,即根 VC 已连接到另一个 VC,而不是显示警报控制器,而是发出了类似于上面报告的警告:

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

    我还没有测试过,但是如果您的根 VC 恰好是导航控制器,这也可能是必要的。

    【讨论】:

    • 嗯,我在 Swift 中遇到了这个问题,我不知道如何将你的 objc 代码翻译成 swift,不胜感激!
    • @Mayerz 将 Objective-C 翻译成 Swift 应该没什么大不了的 ;) 但你在这里:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
    • 感谢 Olivier,你说得对,这很容易,我确实是这样翻译的,但问题出在其他地方。还是谢谢!
    • Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (&lt;UIAlertController: 0x15cd4afe0&gt;)
    • 我采用了相同的方法,如果它不为零,则使用rootViewController.presentedViewController,否则使用rootViewController。对于一个完全通用的解决方案,可能需要遍历presentedViewControllers 链才能到达topmost VC
    【解决方案11】:

    @agilityvision 的答案已翻译为 Swift4/iOS11。我没有使用本地化字符串,但您可以轻松更改:

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

    【讨论】:

    • 我得到了一个黑色背景和接受的答案。 window.backgroundColor = UIColor.clear 解决了这个问题。 viewController.view.backgroundColor = UIColor.clear 似乎没有必要。
    • 请记住,Apple 会警告 UIAlertController 子类化:The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified. developer.apple.com/documentation/uikit/uialertcontroller
    【解决方案12】:

    斯威夫特 5

    显示消息后隐藏窗口很重要。

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

    【讨论】:

    • 隐藏 alertWindow 会删除你创建的 UIViewController 吗?我认为最好像这样关闭 rootViewController alertWindow.rootViewController?.dismiss(animated: false)
    • 这是最好的,但是在关闭处理程序中你应该alertWindow.removeFromSuperview() 不要让他们在每次调用根函数时继续添加新的 UIWindow()。
    【解决方案13】:

    对于 iOS 13,以 mythicalcoderbobbyrehm 的答案为基础:

    在 iOS 13 中,如果您要创建自己的窗口来显示警报,则需要对该窗口进行强引用,否则您的警报将不会显示,因为该窗口将在其引用时立即释放退出范围。

    此外,您需要在解除警报后再次将引用设置为 nil,以便移除窗口以继续允许用户在其下方的主窗口上进行交互。

    可以创建UIViewController子类来封装窗口内存管理逻辑:

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

    您可以按原样使用它,或者如果您想在 UIAlertController 上使用方便的方法,您可以将其放入扩展程序中:

    extension UIAlertController {
    
        func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
    
            let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
            windowAlertPresentationController.present(animated: animated, completion: completion)
        }
    }
    

    【讨论】:

    • 如果您需要手动关闭警报,这将不起作用 - WindowAlertPresentationController 永远不会被解除分配,导致 UI 冻结 - 由于窗口仍然存在,因此没有任何交互性
    • 如果您想手动解除警报,请确保直接在 WindowAlertPresentationController 上调用dismiss alert.presentingViewController?.dismiss(animated: true, completion: nil)
    • 让 alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert); alertController.presentInOwnWindow(animated: false, completion: nil) 非常适合我!谢谢!
    • 这适用于装有 iOS 12.4.5 的 iPhone 6,但不适用于装有 iOS 13.3.1 的 iPhone 11 Pro。没有错误,但永远不会显示警报。任何建议将不胜感激。
    • 适用于 iOS 13。在 Catalyst 中不起作用——一旦解除警报,应用程序将无法交互。查看@Peter Lapisu 的解决方案
    【解决方案14】:

    像 Aviel Gross 的答案一样创建扩展。这里你有 Objective-C 扩展。

    这里有头文件 *.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
    

    以及实现:*.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
    

    您在您的实现文件中使用此扩展,如下所示:

    #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];
    

    【讨论】:

      【解决方案15】:

      Swift 4+

      我使用多年的解决方案完全没有问题。首先我扩展UIWindow 以找到它的visibleViewController。 注意:如果您使用自定义集合* 类(例如侧边菜单),您应该在以下扩展中为此案例添加处理程序。获得最顶级的视图控制器后,很容易呈现UIAlertController,就像UIAlertView

      extension UIAlertController {
      
        func show(animated: Bool = true, completion: (() -> Void)? = nil) {
          if let visibleViewController = UIApplication.shared.keyWindow?.visibleViewController {
            visibleViewController.present(self, animated: animated, completion: completion)
          }
        }
      
      }
      
      extension UIWindow {
      
        var visibleViewController: UIViewController? {
          guard let rootViewController = rootViewController else {
            return nil
          }
          return visibleViewController(for: rootViewController)
        }
      
        private func visibleViewController(for controller: UIViewController) -> UIViewController {
          var nextOnStackViewController: UIViewController? = nil
          if let presented = controller.presentedViewController {
            nextOnStackViewController = presented
          } else if let navigationController = controller as? UINavigationController,
            let visible = navigationController.visibleViewController {
            nextOnStackViewController = visible
          } else if let tabBarController = controller as? UITabBarController,
            let visible = (tabBarController.selectedViewController ??
              tabBarController.presentedViewController) {
            nextOnStackViewController = visible
          }
      
          if let nextOnStackViewController = nextOnStackViewController {
            return visibleViewController(for: nextOnStackViewController)
          } else {
            return controller
          }
        }
      
      }
      

      【讨论】:

        【解决方案16】:

        交叉发布我的answer,因为这两个线程没有被标记为骗子...

        现在UIViewController 是响应者链的一部分,您可以执行以下操作:

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

        【讨论】:

          【解决方案17】:

          Zev Eisenberg 的回答简单明了,但并不总是有效,并且可能会失败并显示以下警告消息:

          Warning: Attempt to present <UIAlertController: 0x7fe6fd951e10>  
           on <ThisViewController: 0x7fe6fb409480> which is already presenting 
           <AnotherViewController: 0x7fe6fd109c00>
          

          这是因为 windows rootViewController 不在显示视图的顶部。为了纠正这个问题,我们需要遍历表示链,如我用 Swift 3 编写的 UIAlertController 扩展代码所示:

             /// show the alert in a view controller if specified; otherwise show from window's root pree
          func show(inViewController: UIViewController?) {
              if let vc = inViewController {
                  vc.present(self, animated: true, completion: nil)
              } else {
                  // find the root, then walk up the chain
                  var viewController = UIApplication.shared.keyWindow?.rootViewController
                  var presentedVC = viewController?.presentedViewController
                  while presentedVC != nil {
                      viewController = presentedVC
                      presentedVC = viewController?.presentedViewController
                  }
                  // now we present
                  viewController?.present(self, animated: true, completion: nil)
              }
          }
          
          func show() {
              show(inViewController: nil)
          }
          

          2017 年 9 月 15 日更新:

          经过测试并确认,上述逻辑在新推出的 iOS 11 GM 种子中仍然有效。然而,agilityvision 投票最多的方法却没有:新创建的UIWindow 中显示的警报视图位于键盘下方,可能会阻止用户点击其按钮。这是因为在 iOS 11 中,所有高于键盘窗口的 windowLevels 都会降低到低于它的级别。

          keyWindow 提出的一个人工制品是在出现警报时键盘向下滑动的动画,并在解除警报时再次向上滑动。如果您希望键盘在演示期间停留在那里,您可以尝试从顶部窗口本身进行演示,如下代码所示:

          func show(inViewController: UIViewController?) {
              if let vc = inViewController {
                  vc.present(self, animated: true, completion: nil)
              } else {
                  // get a "solid" window with the highest level
                  let alertWindow = UIApplication.shared.windows.filter { $0.tintColor != nil || $0.className() == "UIRemoteKeyboardWindow" }.sorted(by: { (w1, w2) -> Bool in
                      return w1.windowLevel < w2.windowLevel
                  }).last
                  // save the top window's tint color
                  let savedTintColor = alertWindow?.tintColor
                  alertWindow?.tintColor = UIApplication.shared.keyWindow?.tintColor
          
                  // walk up the presentation tree
                  var viewController = alertWindow?.rootViewController
                  while viewController?.presentedViewController != nil {
                      viewController = viewController?.presentedViewController
                  }
          
                  viewController?.present(self, animated: true, completion: nil)
                  // restore the top window's tint color
                  if let tintColor = savedTintColor {
                      alertWindow?.tintColor = tintColor
                  }
              }
          }
          

          上述代码唯一不太重要的部分是它检查类名UIRemoteKeyboardWindow 以确保我们也可以包含它。尽管如此,上面的代码在 iOS 9、10 和 11 GM 种子中运行良好,具有正确的色调并且没有键盘滑动伪影。

          【讨论】:

          • 刚刚浏览了许多以前的答案,看到了 Kevin Sliech 的答案,它试图用类似的方法解决同样的问题,但没有沿着演示链向上走,因此容易受到与它试图解决的错误相同。
          【解决方案18】:

          在 Objective-C 中显示警报的简写方式:

          [[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];
          

          alertController 是您的 UIAlertController 对象。

          注意:您还需要确保您的助手类扩展 UIViewController

          【讨论】:

            【解决方案19】:

            如果有人感兴趣,我创建了一个 Swift 3 版本的 @agilityvision 答案。代码:

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

            【讨论】:

            • @Chathuranga:我已恢复您的编辑。这种“错误处理”是完全没有必要的。
            【解决方案20】:

            其中一些答案仅对我部分有效,将它们组合在 AppDelegate 中的以下类方法中是我的解决方案。它适用于 iPad、UITabBarController 视图、UINavigationController 以及呈现模式时。在 iOS 10 和 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;
            }
            

            用法:

            [[AppDelegate rootViewController] presentViewController ...
            

            【讨论】:

              【解决方案21】:

              iOS13 场景支持(使用 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
                      }
              
                  }
              
              }
              

              【讨论】:

              • UIAlerController 不应根据文档进行子类化 developer.apple.com/documentation/uikit/uialertcontroller
              • UIButton 也不应该被子类化,每个人都这样做......只要你不修改私有方法并且不更改警报控制器内的视图,就可以了
              • 我找不到对UIButton shouldn't also be sub-classed 的任何引用。可以分享一下源码吗?
              • 谢谢!让 UIScenes 真的搞砸了如何创建窗口,你的方法很好。
              【解决方案22】:

              更新为与 iOS 13 场景一起使用,这打破了新的 UIWindow 方法。斯威夫特 5.1。

              fileprivate var alertWindows = [UIAlertController:UIWindow]()
              
              extension UIAlertController {
              
                  func presentInNewWindow(animated: Bool, completion: (() -> Void)?) {
                      let foregroundActiveScene = UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }.first
                      guard let foregroundWindowScene = foregroundActiveScene as? UIWindowScene else { return }
              
                      let window = UIWindow(windowScene: foregroundWindowScene)
                      alertWindows[self] = window
              
                      window.rootViewController = UIViewController()
                      window.windowLevel = .alert + 1
                      window.makeKeyAndVisible()
                      window.rootViewController!.present( self, animated: animated, completion: completion)
                  }
              
                  open override func viewDidDisappear(_ animated: Bool) {
                      super.viewDidDisappear(animated)
                      alertWindows[self] = nil
                  }
              
              }
              

              【讨论】:

              • viewDidDisappear() 在解除警报后没有被调用。有谁知道为什么?明显的结果是应用程序被冻结
              【解决方案23】:

              Kevin Sliech 提供了一个很好的解决方案。

              我现在在我的主 UIViewController 子类中使用以下代码。

              我做的一个小改动是检查最好的演示控制器是否不是普通的 UIViewController。如果不是,那一定是某个 VC 提供了一个普通的 VC。因此,我们返回正在呈现的 VC。

              - (UIViewController *)bestPresentationController
              {
                  UIViewController *bestPresentationController = [UIApplication sharedApplication].keyWindow.rootViewController;
              
                  if (![bestPresentationController isMemberOfClass:[UIViewController class]])
                  {
                      bestPresentationController = bestPresentationController.presentedViewController;
                  }    
              
                  return bestPresentationController;
              }
              

              到目前为止,在我的测试中似乎一切正常。

              谢谢凯文!

              【讨论】:

                【解决方案24】:
                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
                        }
                    }
                }
                

                有了这个,你可以很容易地像这样展示你的警报

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

                需要注意的是,如果当前正在显示 UIAlertController,UIApplication.topMostViewController 将返回 UIAlertController。在UIAlertController 上展示会出现奇怪的行为,应该避免。因此,您应该在演示之前手动检查 !(UIApplication.topMostViewController is UIAlertController),或者添加 else if 大小写以在 self is UIAlertController 时返回 nil

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

                【讨论】:

                  【解决方案25】:

                  您可以将当前视图或控制器作为参数发送:

                  + (void)myUtilityMethod:(id)controller {
                      // do stuff
                      // something bad happened, display an alert.
                  }
                  

                  【讨论】:

                  • 是的,这是可能的,而且会奏效。但对我来说,它有点代码味道。被调用的方法通常需要传递的参数才能执行其主要功能。此外,所有现有的调用都需要修改。
                  【解决方案26】:

                  除了给出很好的答案(agilityvisionadibmalhal)。要达到像旧 UIAlertViews 中那样的排队行为(避免警报窗口重叠),请使用此块来观察窗口级别的可用性:

                  @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
                  

                  完整示例:

                  [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];
                  }];
                  

                  这将允许您避免警报窗口重叠。可以使用相同的方法为任意数量的窗口层分离和放入队列视图控制器。

                  【讨论】:

                    【解决方案27】:

                    似乎有效:

                    static UIViewController *viewControllerForView(UIView *view) {
                        UIResponder *responder = view;
                        do {
                            responder = [responder nextResponder];
                        }
                        while (responder && ![responder isKindOfClass:[UIViewController class]]);
                        return (UIViewController *)responder;
                    }
                    
                    -(void)showActionSheet {
                        UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
                        [alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
                        [alertController addAction:[UIAlertAction actionWithTitle:@"Do it" style:UIAlertActionStyleDefault handler:nil]];
                        [viewControllerForView(self) presentViewController:alertController animated:YES completion:nil];
                    }
                    

                    【讨论】:

                    • 这其实很巧妙。可能并非在所有情况下都是理想的,特别是如果您使用的子视图控制器在理想情况下不应该呈现视图控制器,但可以对此进行改进。
                    【解决方案28】:

                    我尝试了所有提到的方法,但没有成功。我用于 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)
                            }
                        }
                    }
                    

                    【讨论】:

                      【解决方案29】:

                      另一种选择:

                          var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
                          while ((topController.presentedViewController) != nil) {
                              topController = topController.presentedViewController!
                          }
                          topController.present(alert, animated:true, completion:nil)
                      

                      【讨论】:

                        【解决方案30】:

                        您可以尝试使用类似方法在UIViewController 上实现一个类别 - (void)presentErrorMessage; 在该方法中实现 UIAlertController,然后将其呈现在 self 上。比在您的客户端代码中,您将拥有类似的内容:

                        [myViewController presentErrorMessage];

                        这样,您将避免不必要的参数和关于视图不在窗口层次结构中的警告。

                        【讨论】:

                        • 除非我在发生坏事的代码中没有myViewController。这是一个实用方法,它对调用它的视图控制器一无所知。
                        • 恕我直言,向用户呈现任何视图(因此警报)是 ViewControllers 的责任。因此,如果代码的某些部分对 viewController 一无所知,它不应该向用户显示任何错误,而是将它们传递给代码的“viewController 感知”部分
                        • 我同意。但是现在已弃用的UIAlertView 的便利性让我在一些地方打破了这条规则。
                        猜你喜欢
                        • 1970-01-01
                        • 2020-10-16
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 1970-01-01
                        • 2012-03-16
                        • 1970-01-01
                        相关资源
                        最近更新 更多