ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

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

在这种情况下,您如何在 myUtilityMethod 中显示一个 UIAlertController


C
Community

在 WWDC,我在其中一个实验室停下来,问了一位 Apple 工程师同样的问题:“显示 UIAlertController 的最佳做法是什么?”他说他们经常收到这个问题,我们开玩笑说他们应该就这个问题进行一次会议。他说,Apple 内部正在创建一个带有透明 UIViewControllerUIWindow,然后在其上显示 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];

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

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


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

迅速

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 以来已被弃用
Z
Zev Eisenberg

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

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

和斯威夫特 3.0:

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

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

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

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

当你想显示你的 UIAlertController 时:

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

有几点需要注意:

您的 UIWindow 必须被强引用。如果它没有被强烈引用,它将永远不会出现(因为它已被释放)。我建议使用属性,但我也成功使用了关联对象。

为了确保窗口出现在其他所有内容之上(包括系统 UIAlertControllers),我设置了 windowLevel。 (window.windowLevel = UIWindowLevelAlert + 1)

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

https://github.com/dbettermann/DBAlertController


对于 Objective-C,你没有这个,是吗?
是的,它甚至可以在 Swift 2.0/iOS 9 中使用。我现在正在开发一个 Objective-C 版本,因为其他人要求它(也许是你)。完成后我会回帖。
G
Gerald

对于 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 并设置 {6 } 在 viewDidAppear 上到 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) } }
C
Community

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

完美,正是我需要关闭窗口的提示,谢谢队友
C
Community

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

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

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

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

Quick 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 的强引用,否则窗口将被释放并在超出范围后很快消失。
k
konekoya

这是 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 不可访问,也可以使用它!
我收到错误:对
警报自动消失了?不完全确定它为什么这样做
E
ElOjcar

这在 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
K
Kevin Sliech

添加到 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 (<UIAlertController: 0x15cd4afe0>)
我采用了相同的方法,如果它不是 nil,则使用 rootViewController.presentedViewController,否则使用 rootViewController。对于完全通用的解决方案,可能需要遍历 presentedViewController 链才能到达 topmost VC
D
Dylan Colaco

@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
L
Logan Gauthier

对于 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 的解决方案
e
exp

斯威夫特 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()。
M
Marcin Kapusta

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

T
Timur Bernikovich

斯威夫特 4+

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

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

}

C
Community

交叉发布我的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)
}

C
CodeBrew

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 的答案,它试图用类似的方法解决相同的问题,但没有沿着演示链向上走,因此它容易受到与它试图解决的相同的错误的影响.
E
Eerko

其中一些答案仅对我部分有效,将它们组合在 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 ...

V
ViperMav

在 Objective-C 中显示警报的速记方法:

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

其中 alertController 是您的 UIAlertController 对象。

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


M
Martin R

如果有人感兴趣,我创建了一个 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:我已恢复您的编辑。这种“错误处理”是完全没有必要的。
P
Peter Lapisu

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 真的搞砸了如何创建窗口,你的方法很好。
D
December

更新为与 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() 。有谁知道为什么?明显的结果是应用程序被冻结
c
computingfreak

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

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

谢谢凯文!


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

P
Pablo A.

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

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

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

除了给出很好的答案(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];
}];

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


w
wonder.mice

似乎工作:

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

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

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

M
Mike Koene

另外的选择:

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

G
Gralex

您可以使用 2 种方法:

- 改用 UIAlertView 或 'UIActionSheet'(不推荐,因为它在 iOS 8 中已弃用,但现在可以使用)

- 以某种方式记住呈现的最后一个视图控制器。这是示例。

@interface UIViewController (TopController)
+ (UIViewController *)topViewController;
@end

// implementation

#import "UIViewController+TopController.h"
#import <objc/runtime.h>

static __weak UIViewController *_topViewController = nil;

@implementation UIViewController (TopController)

+ (UIViewController *)topViewController {
    UIViewController *vc = _topViewController;
    while (vc.parentViewController) {
        vc = vc.parentViewController;
    }
    return vc;
}

+ (void)load {
    [super load];
    [self swizzleSelector:@selector(viewDidAppear:) withSelector:@selector(myViewDidAppear:)];
    [self swizzleSelector:@selector(viewWillDisappear:) withSelector:@selector(myViewWillDisappear:)];
}

- (void)myViewDidAppear:(BOOL)animated {
    if (_topViewController == nil) {
        _topViewController = self;
    }

    [self myViewDidAppear:animated];
}

- (void)myViewWillDisappear:(BOOL)animated {
    if (_topViewController == self) {
        _topViewController = nil;
    }

    [self myViewWillDisappear:animated];
}

+ (void)swizzleSelector:(SEL)sel1 withSelector:(SEL)sel2
{
    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, sel1);
    Method swizzledMethod = class_getInstanceMethod(class, sel2);

    BOOL didAddMethod = class_addMethod(class,
                                        sel1,
                                        method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        class_replaceMethod(class,
                            sel2,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end 

用法:

[[UIViewController topViewController] presentViewController:alertController ...];