无论视图层次结构如何,都要在顶部显示UIAlertController

53

我想创建一个帮助类,用于展示UIAlertController。由于它是一个辅助类,我希望它能够在任何视图层次结构中使用,而且不需要关于视图的任何信息。我可以显示警告,但当警告被取消时,应用程序崩溃并出现以下错误:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Trying to dismiss UIAlertController <UIAlertController: 0x135d70d80>
 with unknown presenter.'

我正在使用以下代码创建弹出窗口:

guard let window = UIApplication.shared.keyWindow else { return }
let view = UIView()
view.isUserInteractionEnabled = true
window.insertSubview(view, at: 0)
window.bringSubview(toFront: view)
// add full screen constraints to view ...

let controller = UIAlertController(
  title: "confirm deletion?",
  message: ":)",
  preferredStyle: .alert
)

let deleteAction = UIAlertAction(
  title: "yes",
  style: .destructive,
  handler: { _ in
    DispatchQueue.main.async {
      view.removeFromSuperview()
      completion()
    }
  }
)
controller.addAction(deleteAction)

view.insertSubview(controller.view, at: 0)
view.bringSubview(toFront: controller.view)
// add centering constraints to controller.view ...

当我点击yes时,应用程序会崩溃并且处理程序在崩溃之前没有被触发。我无法呈现UIAlertController,因为这将取决于当前视图层次结构,而我希望弹出窗口是独立的。

编辑:Swift解决方案 感谢@Vlad提供的想法。似乎在单独的窗口中操作要简单得多。所以这里是一个可行的Swift解决方案:

class Popup {
  private var alertWindow: UIWindow
  static var shared = Popup()

  init() {
    alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindowLevelAlert + 1
    alertWindow.makeKeyAndVisible()
    alertWindow.isHidden = true
  }

  private func show(completion: @escaping ((Bool) -> Void)) {
    let controller = UIAlertController(
      title: "Want to do it?",
      message: "message",
      preferredStyle: .alert
    )

    let yesAction = UIAlertAction(
      title: "Yes",
      style: .default,
      handler: { _ in
        DispatchQueue.main.async {
          self.alertWindow.isHidden = true
          completion(true)
        }
    })

    let noAction = UIAlertAction(
      title: "Not now",
      style: .destructive,
      handler: { _ in
        DispatchQueue.main.async {
          self.alertWindow.isHidden = true
          completion(false)
        }
    })

    controller.addAction(noAction)
    controller.addAction(yesAction)
    self.alertWindow.isHidden = false
    alertWindow.rootViewController?.present(controller, animated: false)
  }
}
12个回答

111

2019年12月16日更新:

只需从当前最上层的视图控制器中呈现视图控制器/警报即可。那样就可以正常工作:)

if #available(iOS 13.0, *) {
     if var topController = UIApplication.shared.keyWindow?.rootViewController  {
           while let presentedViewController = topController.presentedViewController {
                 topController = presentedViewController
                }
     topController.present(self, animated: true, completion: nil)
}

更新于2019年7月23日:

重要提示

显然,下面这种方法在iOS 13.0中已经无法使用 :(

一旦我有时间调查,我会进行更新...

旧技术:

这是Swift(5)的扩展:

public extension UIAlertController {
    func show() {
        let win = UIWindow(frame: UIScreen.main.bounds)
        let vc = UIViewController()
        vc.view.backgroundColor = .clear
        win.rootViewController = vc
        win.windowLevel = UIWindow.Level.alert + 1  // Swift 3-4: UIWindowLevelAlert + 1
        win.makeKeyAndVisible()    
        vc.present(self, animated: true, completion: nil)
    }
}

只需设置您的 UIAlertController,然后调用:

alert.show()

不再受视图控制器层次结构的限制!


2
真的,这太完美了。不错。 - Fattie
17
那么你该如何隐藏它? - Erik Grosskurth
1
如果您的应用程序仅支持一种方向,则在新窗口上显示警报看起来很好。但是,如果您想处理UI旋转,特别是如果不同的视图控制器具有自己支持的方向配置,则需要做更多的工作。事实上,我还没有找到一个能够很好地处理方向的解决方案。 - Henry H Miao
2
在Xcode 11和iOS 13 beta中,我已经使用很长时间的答案会导致警报显示,然后在大约0.5秒后消失。有没有人在新beta版本中有足够的时间来知道原因? - Matthew Bradshaw
1
@MatthewBradshaw 我遇到了同样的消失问题。问题在于窗口实例是局部函数的,一旦函数运行完毕,窗口就会被释放。 - Lance Samaria
显示剩余8条评论

17

我更倾向于在UIApplication.shared.keyWindow.rootViewController上呈现它,而不是使用你的逻辑。所以你可以这样做:

UIApplication.shared.keyWindow.rootViewController.presentController(yourAlert, animated: true, completion: nil)

编辑过:

我有一个旧的ObjC类别,其中我使用了下一个show方法,如果没有提供控制器来展示,则使用该方法:

- (void)show
{
    self.alertWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
    self.alertWindow.rootViewController = [UIViewController new];
    self.alertWindow.windowLevel = UIWindowLevelAlert + 1;
    [self.alertWindow makeKeyAndVisible];
    [self.alertWindow.rootViewController presentViewController: self animated: YES completion: nil];
}

添加了整个分类,如果有人需要的话。

#import "UIAlertController+ShortMessage.h"
#import <objc/runtime.h>

@interface UIAlertController ()
@property (nonatomic, strong) UIWindow* alertWindow;
@end

@implementation UIAlertController (ShortMessage)

- (void)setAlertWindow: (UIWindow*)alertWindow
{
    objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIWindow*)alertWindow
{
    return objc_getAssociatedObject(self, @selector(alertWindow));
}

+ (UIAlertController*)showShortMessage: (NSString*)message fromController: (UIViewController*)controller
{
    return [self showAlertWithTitle: nil shortMessage: message fromController: controller];
}

+ (UIAlertController*)showAlertWithTitle: (NSString*)title shortMessage: (NSString*)message fromController: (UIViewController*)controller
{
    return [self showAlertWithTitle: title shortMessage: message actions: @[[UIAlertAction actionWithTitle: @"Ok" style: UIAlertActionStyleDefault handler: nil]] fromController: controller];
}

+ (UIAlertController*)showAlertWithTitle: (NSString*)title shortMessage: (NSString*)message actions: (NSArray<UIAlertAction*>*)actions fromController: (UIViewController*)controller
{
    UIAlertController* alert = [UIAlertController alertControllerWithTitle: title
                                                    message: message
                                             preferredStyle: UIAlertControllerStyleAlert];

    for (UIAlertAction* action in actions)
    {
        [alert addAction: action];
    }

    if (controller)
    {
        [controller presentViewController: alert animated: YES completion: nil];
    }
    else
    {
        [alert show];
    }

    return alert;
}

+ (UIAlertController*)showAlertWithMessage: (NSString*)message actions: (NSArray<UIAlertAction*>*)actions fromController: (UIViewController*)controller
{
    return [self showAlertWithTitle: @"" shortMessage: message actions: actions fromController: controller];
}

- (void)show
{
    self.alertWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
    self.alertWindow.rootViewController = [UIViewController new];
    self.alertWindow.windowLevel = UIWindowLevelAlert + 1;
    [self.alertWindow makeKeyAndVisible];
    [self.alertWindow.rootViewController presentViewController: self animated: YES completion: nil];
}

@end

它会在其他所有东西的上方呈现吗? - Guig
对我来说,这总是有效的:) 如果你不尝试,你永远不会知道 :) - Vladyslav Zavalykhatko
谢谢。窗口的功能相当不错。完成后,我必须隐藏窗口才能与其他元素进行交互。 - Guig
4
如果您有一个以模态方式呈现的视图控制器,则您的代码将无法在其前面显示UIAlertController。 - sazz

11

在iOS 13中,旧方法添加show()方法和本地UIWindow实例的方法已不再适用(窗口立即被取消)。

这是一个UIAlertController的Swift扩展,应该可以在iOS 13上使用:

import UIKit

private var associationKey: UInt8 = 0

extension UIAlertController {

    private var alertWindow: UIWindow! {
        get {
            return objc_getAssociatedObject(self, &associationKey) as? UIWindow
        }

        set(newValue) {
            objc_setAssociatedObject(self, &associationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }

    func show() {
        self.alertWindow = UIWindow.init(frame: UIScreen.main.bounds)
        self.alertWindow.backgroundColor = .red

        let viewController = UIViewController()
        viewController.view.backgroundColor = .green
        self.alertWindow.rootViewController = viewController

        let topWindow = UIApplication.shared.windows.last
        if let topWindow = topWindow {
            self.alertWindow.windowLevel = topWindow.windowLevel + 1
        }

        self.alertWindow.makeKeyAndVisible()
        self.alertWindow.rootViewController?.present(self, animated: true, completion: nil)
    }

    override open func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        self.alertWindow.isHidden = true
        self.alertWindow = nil
    }
}

可以这样创建并展示UIAlertController:

let alertController = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "Title", style: .default) { (action) in
    print("Action")
}

alertController.addAction(alertAction)
alertController.show()

1
这个解决方案在我的iOS13上有效。干得好,谢谢 :) - Yogesh Patel
3
我想警告你这个解决方案。由于该扩展覆盖了viewDidDisappear方法,如果你没有使用show()方法来呈现视图,这会导致应用崩溃。 - Simon C.
3
可以通过将 alertWindow 转换为可选类型,或者在 viewDidDisappear 中添加 assert(以便开发人员理解必须先调用 show() 方法),来轻松解决此问题。我的建议是:在使用 API 之前确保您了解其工作原理 :) - Maxim Makhun

9

在Swift 4.1和Xcode 9.4.1中,

我从我的共享类调用警报函数。

//This is my shared class
import UIKit

class SharedClass: NSObject {

static let sharedInstance = SharedClass()
    //This is alert function
    func alertWindow(title: String, message: String) {
        let alertWindow = UIWindow(frame: UIScreen.main.bounds)
        alertWindow.rootViewController = UIViewController()
        alertWindow.windowLevel = UIWindowLevelAlert + 1

        let alert2 = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let defaultAction2 = UIAlertAction(title: "OK", style: .default, handler: { action in
        })
        alert2.addAction(defaultAction2)

        alertWindow.makeKeyAndVisible()
        alertWindow.rootViewController?.present(alert2, animated: true, completion: nil)
    }
    private override init() {
    }
}

我在所需的视图控制器中调用这个警告函数,就像这样。
//I'm calling this function into my second view controller
SharedClass.sharedInstance.alertWindow(title:"Title message here", message:"Description message here")

4

Swift 3 示例

let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1

let alert = UIAlertController(title: "AlertController Tutorial", message: "Submit something", preferredStyle: .alert)

alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)

4

如果你试图在以模态方式呈现的UIViewController中展示UIActivityController,则需要从presentedViewController中进行呈现。否则没有东西会被呈现。我在iOS 13中使用这种方法返回活动的UIViewController:

func activeVC() -> UIViewController? {
    // Use connectedScenes to find the .foregroundActive rootViewController
    var rootVC: UIViewController?
    for scene in UIApplication.shared.connectedScenes {
        if scene.activationState == .foregroundActive {
            rootVC = (scene.delegate as? UIWindowSceneDelegate)?.window!!.rootViewController
            break
        }
    }
    // Then, find the topmost presentedVC from it.
    var presentedVC = rootVC
    while presentedVC?.presentedViewController != nil {
        presentedVC = presentedVC?.presentedViewController
    }
    return presentedVC
}

所以,例如:
activeVC()?.present(activityController, animated: true)

4

结合Ruslan和Steve的回答,这适用于我在iOS 13.1和Xcode 11.5上的情况。

func activeVC() -> UIViewController? {

let appDelegate = UIApplication.shared.delegate as! AppDelegate

var topController: UIViewController = appDelegate.window!.rootViewController!
while (topController.presentedViewController != nil) {
    topController = topController.presentedViewController!
}

return topController 
}

用途:

activeVC()?.present(alert, animated: true)

4
经常被引用的使用新创建的UIWindow作为UIAlertController扩展的解决方案在iOS 13 Beta中停止工作(看起来iOS不再对UIWindow保持强引用,所以警报立即消失)。
下面的解决方案略微复杂,但适用于iOS 13.0和旧版本的iOS:
class GBViewController: UIViewController {
    var didDismiss: (() -> Void)?
    override func dismiss(animated flag: Bool, completion: (() -> Void)?)
    {
        super.dismiss(animated: flag, completion:completion)
        didDismiss?()
    }
    override var prefersStatusBarHidden: Bool {
        return true
    }
}

class GlobalPresenter {
    var globalWindow: UIWindow?
    static let shared = GlobalPresenter()

    private init() {
    }

    func present(controller: UIViewController) {
        globalWindow = UIWindow(frame: UIScreen.main.bounds)
        let root = GBViewController()
        root.didDismiss = {
            self.globalWindow?.resignKey()
            self.globalWindow = nil
        }
        globalWindow!.rootViewController = root
        globalWindow!.windowLevel = UIWindow.Level.alert + 1
        globalWindow!.makeKeyAndVisible()
        globalWindow!.rootViewController?.present(controller, animated: true, completion: nil)
    }
}

使用方法

    let alert = UIAlertController(title: "Alert Test", message: "Alert!", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    GlobalPresenter.shared.present(controller: alert)

对我来说这个不起作用...我得到了错误信息“不平衡的调用,什么也没有发生”。 - anoop4real

3

我自己的iOS 13解决方法。

编辑通知:我编辑了我的先前答案,因为其他解决方案都使用了viewWillDisappear:的重新定义,这在类扩展中是不正确的,并且在13.4中实际上已经停止工作。

这个解决方案基于UIWindow范例,在UIAlertController上定义了一个类别(扩展)。在该类别文件中,我们还定义了UIViewController的简单子类,用于呈现UIAlertController

@interface AlertViewControllerPresenter : UIViewController
@property UIWindow *win;
@end

@implementation AlertViewControllerPresenter
- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    [_win resignKeyWindow]; //optional nilling the window works
    _win.hidden = YES; //optional nilling the window works
    _win = nil;
    [super dismissViewControllerAnimated:flag completion:completion];
}
@end

演示者保留窗口。当呈现的警报被解除时,窗口将被释放。

然后在类别(扩展)中定义一个show方法:

- (void)show {
    AlertViewControllerPresenter *vc = [[AlertViewControllerPresenter alloc] init];
    vc.view.backgroundColor = UIColor.clearColor;
    UIWindow *win = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    vc.win = win;
    win.rootViewController = vc;
    win.windowLevel = UIWindowLevelAlert;
    [win makeKeyAndVisible];
    [vc presentViewController:self animated:YES completion:nil];
}

我知道这篇文章的标签是Swift,但是它很容易适应...


3

针对TVOS 13和iOS 13的工作解决方案

static func showOverAnyVC(title: String, message: String) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
    alert.addAction((UIAlertAction(title: "OK", style: .default, handler: {(action) -> Void in
    })))
    let appDelegate = UIApplication.shared.delegate as! AppDelegate

    var topController: UIViewController = appDelegate.window!.rootViewController!
    while (topController.presentedViewController != nil) {
        topController = topController.presentedViewController!
    }

    topController.present(alert, animated: true, completion: nil)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接