ios 不在视图控制器中时如何呈现 UIAlertController?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26554894/
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 03:20:08  来源:igfitidea点击:

How to present UIAlertController when not in a view controller?

iosuialertcontroller

提问by Murray Sagal

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 called on another class. A bad thing happens there and I want to display an alert right there before control returns to the view controller.

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

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

This was possible with UIAlertView(but perhaps not quite proper).

这是可能的UIAlertView(但可能不太合适)。

In this case, how do you present a UIAlertController, right there in myUtilityMethod?

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

采纳答案by Dylan Bettermann

I posted a similar questiona 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.

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

When you want to display your UIAlertController:

当你想显示你的 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
  1. 使您的窗口成为关键和可见窗口 ( window.makeKeyAndVisible())
  2. 只需使用一个普通的 UIViewController 实例作为新窗口的 rootViewController 即可。( window.rootViewController = UIViewController())
  3. 在窗口的 rootViewController 上显示您的 UIAlertController

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)
  • 你的 UIWindow 必须被强烈引用。如果它没有被强烈引用,它将永远不会出现(因为它被释放了)。我建议使用属性,但我也成功使用了关联的 object
  • 为了确保窗口出现在其他所有东西之上(包括系统 UIAlertControllers),我设置了 windowLevel。( window.windowLevel = UIWindowLevelAlert + 1)

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

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

https://github.com/dbettermann/DBAlertController

https://github.com/dbettermann/DBAlertController

回答by agilityvision

At WWDC, I stopped in at one of the labs and asked an Apple Engineer this same question: "What was the best practice for displaying a UIAlertController?" And he said they had been getting this question a lot and we joked that they should have had a session on it. He said that internally Apple is creating a UIWindowwith a transparent UIViewControllerand then presenting the UIAlertControlleron it. Basically what is in Dylan Betterman's answer.

在 WWDC,我在其中一个实验室停下来问了一位 Apple 工程师同样的问题:“显示UIAlertController. 他说他们经常收到这个问题,我们开玩笑说他们应该就这个问题进行一次会议。他说,苹果内部正在创建UIWindow一个透明的UIViewController,然后UIAlertController在上面呈现。基本上是 Dylan Betterman 的回答。

But I didn't want to use a subclass of UIAlertControllerbecause that would require me changing my code throughout my app. So with the help of an associated object, I made a category on UIAlertControllerthat provides a showmethod in Objective-C.

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

Here is the relevant code:

这是相关的代码:

#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

Here is a sample usage:

这是一个示例用法:

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

The UIWindowthat is created will be destroyed when the UIAlertControlleris dealloced, since it is the only object that is retaining the UIWindow. But if you assign the UIAlertControllerto a property or cause its retain count to increase by accessing the alert in one of the action blocks, the UIWindowwill stay on screen, locking up your UI. See the sample usage code above to avoid in the case of needing to access UITextField.

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

I made a GitHub repo with a test project: FFGlobalAlertController

我用一个测试项目制作了一个 GitHub仓库FFGlobalAlertController

回答by Darkngs

Swift

迅速

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

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

回答by Zev Eisenberg

You can do the following with Swift 2.2:

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

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

And Swift 3.0:

和 Swift 3.0:

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

回答by Aviel Gross

Pretty generic UIAlertControllerextensionfor all cases of UINavigationControllerand/or UITabBarController. Also works if there's a modal VC on screen at the moment.

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

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

回答by adib

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.

改进agilityvision 的答案,您需要创建一个带有透明根视图控制器的窗口,并从那里显示警报视图。

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

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

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

回答by mythicalcoder

The following solution did notwork even though it looked quite promising with all the versions. This solution is generating WARNING.

下面的解决方案都没有,即使它看起来相当与所有的版本有前途的工作。此解决方案正在生成 WARNING

Warning:Attempt to present on whose view is not in the window hierarchy!

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

https://stackoverflow.com/a/34487871/2369867=> This is looked promising then. But it was notin Swift 3. So I am answering this in Swift 3 and this is nottemplate example.

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

This is rather fully functional code by itself once you paste inside any function.

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

Quick Swift 3self-containedcode

快速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)

This is tested and working code in Swift 3.

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

回答by bobbyrehm

Here's mythicalcoder's answeras an extension, tested & working in Swift 4:

这是mythicalcoder作为扩展的答案,在 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)
    }

}

Example usage:

用法示例:

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

回答by William Entriken

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

这在 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)

回答by Kevin Sliech

Adding on to Zev's answer (and switching back to Objective-C), you could run into a situation where your root view controller is presenting some other VC via a segue or something else. Calling presentedViewController on the root VC will take care of this:

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

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

This straightened out an issue I had where the root VC had segued to another VC, and instead of presenting the alert controller, a warning like those reported above was issued:

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

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

I haven't tested it, but this may also be necessary if your root VC happens to be a navigation controller.

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