ios - 无论视图层次结构如何,都在所有内容之上显示 UIAlertController

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/40991450/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-31 10:54:22  来源:igfitidea点击:

ios - present UIAlertController on top of everything regardless of the view hierarchy

iosswiftcrashpopup

提问by Guig

I'm trying to have an helper class that presents an UIAlertController. Since it's a helper class, I want it to work regardless of the view hierarchy, and with no information about it. I'm able to show the alert, but when it's being dismissed, the app crashed with:

我正在尝试创建一个提供UIAlertController. 因为它是一个辅助类,所以我希望它在不考虑视图层次结构的情况下工作,并且没有关于它的信息。我能够显示警报,但是当它被关闭时,应用程序崩溃了:

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

I'm creating the popup with:

我正在创建弹出窗口:

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

When I tap yes, the app will crash and the handler is not being hit before the crash. I can't present the UIAlertControllerbecause this would be dependent of the current view hierarchy, while I want the popup to be independant

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

EDIT: Swift solution Thanks @Vlad for the idea. It seems that operating in a separate window is much more simple. So here is a working Swift solution:

编辑: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)
  }
}

回答by jazzgil

Update Dec 16, 2019:

2019 年 12 月 16 日更新:

Just present the view controller/alert from the current top-most view controller. That will work :)

只需从当前最顶层的视图控制器呈现视图控制器/警报。那可行 :)

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

Update July 23, 2019:

2019 年 7 月 23 日更新:

IMPORTANT

重要的

Apparently the method below this technique stopped working in iOS 13.0:(

显然,此技术下面的方法在 iOS 13.0 中停止工作:(

I'll update once I find the time to investigate...

一旦我找到时间调查,我会更新......

Old technique:

旧技术:

Here's a Swift (5) extension for it:

这是它的 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)
    }
}

Just setup your UIAlertController, and then call:

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

alert.show()

No more bound by the View Controllers hierarchy!

不再受 View Controllers 层次结构的束缚!

回答by Vladyslav Zavalykhatko

I will rather present it on UIApplication.shared.keyWindow.rootViewController, instead of using your logic. So you can do next:

我宁愿将它呈现在 UIApplication.shared.keyWindow.rootViewController 上,而不是使用您的逻辑。所以你可以做下一步:

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

EDITED:

编辑:

I have an old ObjC category, where I've used the next method show, which I used, if no controller was provided to present from:

我有一个旧的 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];
}

added entire category, if somebody need it

添加整个类别,如果有人需要它

#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

回答by Maxim Makhun

Old approach with adding show()method and local instance of UIWindowno longer works on iOS 13 (window is dismissed right away).

添加show()方法和本地实例的旧方法UIWindow不再适用于 iOS 13(窗口立即关闭)。

Here is UIAlertControllerSwift extension which should work on iOS 13:

这是适用于 iOS 13 的UIAlertControllerSwift 扩展:

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

Such UIAlertControllerthen can be created and shown like this:

这样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()

回答by iOS

in Swift 4.1 and Xcode 9.4.1

在 Swift 4.1 和 Xcode 9.4.1 中

I'm calling alert function from my shared class

我正在从我的共享类调用警报功能

//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 alert function in my required view controller like this.

我正在像这样在我需要的视图控制器中调用这个警报函数。

//I'm calling this function into my second view controller
SharedClass.sharedInstance.alertWindow(title:"Title message here", message:"Description message here")

回答by AndreasB

The often cited solution using a newly created UIWindowas a UIAlertControllerextension stopped working in iOS 13 Betas (looks like there is no strong reference held by iOS to the UIWindowanymore, so the alert disappears immediately).

使用新创建UIWindowUIAlertController扩展程序经常被引用的解决方案在 iOS 13 Betas 中停止工作(看起来 iOSUIWindow不再拥有对 的强引用,因此警报立即消失)。

The below solution is slightly more complex, but works in iOS 13.0 and older versions of iOS:

下面的解决方案稍微复杂一些,但适用于 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)
    }
}

Usage

用法

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

回答by Lijith Vipin

Swift 3example

斯威夫特 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)

回答by Max_B

My own iOS 13 workaround.

我自己的 iOS 13 解决方法。

Edit notice: I edited my previous answer because, as others solutions around, it used a redefinition of viewWillDisappear:which is incorrect in a class extension and effectively stoped working with 13.4.

编辑通知:我编辑了我之前的答案,因为与周围的其他解决方案一样,它viewWillDisappear:在类扩展中使用了不正确的重新定义,并有效地停止了使用 13.4。

This solution, based on UIWindow paradigm, defines a category (extension) on UIAlertController. In that category file we also define a simple subclass of UIViewControllerused to present theUIAlertController.

该解决方案基于 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

The presenter retains the window. When the presented alert is dismissed the window is released.

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

Then define a show method in the category (extension) :

然后在类别(扩展)中定义一个 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];
}

I do realise that the OP tagged Swift and this is ObjC, but this is so straightforward to adapt…

我确实意识到 OP 标记了 Swift,这是 ObjC,但这很容易适应……

回答by Ruslan Sabirov

working solution for TVOS 13 and iOS 13

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

回答by anuraagdjain

 func windowErrorAlert(message:String){
    let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
    let window = UIWindow(frame: UIScreen.main.bounds)
    window.rootViewController = UIViewController()
    let okAction = UIAlertAction(title: "Ok", style: .default) { (action) -> Void in
        alert.dismiss(animated: true, completion: nil)
        window.resignKey()
        window.isHidden = true
        window.removeFromSuperview()
        window.windowLevel = UIWindowLevelAlert - 1
        window.setNeedsLayout()
    }

    alert.addAction(okAction)
    window.windowLevel = UIWindowLevelAlert + 1
    window.makeKeyAndVisible()

    window.rootViewController?.present(alert, animated: true, completion: nil)
}

Create a UIAlertController on top of all view and also dismiss and give focus back to your rootViewController.

在所有视图之上创建一个 UIAlertController 并关闭并将焦点返回给您的 rootViewController。