当不在视图控制器中时,如何显示UIAlertController?


255

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

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

这是可能的UIAlertView(但可能并不完全正确)。

在这种情况下,你如何呈现UIAlertController的,就在那里myUtilityMethod

Answers:


34

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

解决方案是使用附加的UIWindow。

当您想要显示您的UIAlertController时:

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

需要注意的几件事:

  • 您的UIWindow必须被强烈引用。如果引用不严格,它将永远不会出现(因为它已发布)。我建议使用属性,但是我也成功使用了关联的对象
  • 为了确保该窗口出现在所有其他窗口(包括系统UIAlertControllers)的上方,我设置了windowLevel。(window.windowLevel = UIWindowLevelAlert + 1

最后,如果您只想看一下,我已经完成了实施。

https://github.com/dbettermann/DBAlertController


您没有Objective-C的功能,对吗?
SAHM 2015年

2
是的,它甚至可以在Swift 2.0 / iOS 9中运行。我正在开发一个Objective-C版本,因为有人要求它(也许是您)。完成后我会回发。
Dylan Bettermann

322

在WWDC上,我在其中一个实验室停下来,并向苹果工程师询问了相同的问题:“显示的最佳做法是UIAlertController什么?” 他说,他们已经收到了很多这个问题,我们开玩笑说他们应该开会。他说,Apple内部正在创建UIWindow透明的UIViewController,然后在其UIAlertController上展示。基本上,迪伦·贝特曼(Dylan Betterman)的回答是什么。

但是我不想使用of的子类,UIAlertController因为那将需要我在整个应用程序中更改代码。因此,在相关对象的帮助下,我对其进行了分类,UIAlertController从而提供了showObjective-C中的方法。

以下是相关代码:

#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的dealloced,因为它是被保留的唯一对象UIWindow。但是,如果您将其分配UIAlertController给属性或通过访问其中一个操作块中的警报而导致其保留计数增加,则该属性UIWindow将保留在屏幕上,从而锁定UI。请参阅上面的示例用法代码,以避免需要访问的情况UITextField

我通过测试项目FFGlobalAlertController制作了GitHub 存储库


1
好东西!只是一些背景-我使用子类而不是关联的对象,因为我使用的是Swift。关联对象是Objective-C运行时的功能,我不想依赖它。Swift可能距离获得自己的运行时还需要几年的时间,但仍然如此。:)
Dylan Bettermann

1
我非常喜欢您回答的优美性,但是我很好奇您如何退役新窗口,并再次将原始窗口用作键(诚然,我不太喜欢使用该窗口)。
Dustin Pfannenstiel

1
关键窗口是最上方的可见窗口,所以我的理解是,如果您删除/隐藏“关键”窗口,则下一个可见窗口将变为“关键”。
agilityvision

19
viewDidDisappear:在类别上实施似乎是个坏主意。本质上,您正在与的框架实现竞争viewDidDisappear:。现在可能还可以,但是如果Apple决定将来实现该方法,则您无法调用该方法(即,没有类似之处super指向类别实现中方法的主要实现) 。
adib

5
伟大的作品,但如何对待prefersStatusBarHiddenpreferredStatusBarStyle没有额外的子类?
凯文·弗拉克斯曼

109

迅速

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)

物镜

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

2
+1这是一个非常简单的解决方案。(我面临的问题:在“主/明细”模板的DetailViewController中显示警报-在iPad上显示,从不在iPhone上显示)
大卫

8
很好,您可能需要添加另一部分:if(rootViewController.presentedViewController!= nil){rootViewController = rootViewController.presentedViewController; }
DivideByZer0

1
Swift 3:“警报”已重命名为“警报”:let alertController = UIAlertController(title:“ title”,message:“ message”,preferredStyle:.alert)
Kaptain

请改用委托!
安德鲁·基尔纳

104

您可以使用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)

12
糟糕,我在检查之前接受了。该代码返回根视图控制器,在我的情况下是导航控制器。它不会导致错误,但是不会显示警报。
Murray Sagal

22
我在控制台中注意到:Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!
Murray Sagal

1
@MurraySagal具有导航控制器,您可以随时获取visibleViewController属性,以查看从哪个控制器发出警报。看看文档
Lubo 2016年

2
我这样做是因为我不想因为别人的工作而获得荣誉。这是@ZevEisenberg的解决方案,我针对Swift 3.0进行了修改。如果我还要添加另一个答案,那么我可能会得到他应得的投票。
jeet.chanchawat

1
哦,嘿,我昨天错过了所有戏剧,但是我碰巧刚刚更新了Swift 3的帖子。只要答案是正确的!
Zev Eisenberg

34

UIAlertController extension对于UINavigationController和/或的所有情况都相当通用UITabBarController。如果当前屏幕上有模式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)
          }
        }
    }
}

1
我正在使用此解决方案,但发现它确实完美,优雅,干净...但是,最近我不得不将根视图控制器更改为不在视图层次结构中的视图,因此该代码变得无用。任何人都在想继续使用dix吗?

1
我将此解决方案与其他方法结合使用:我有一个单例UI类,该类具有(弱!) currentVC类型UIViewController。我有BaseViewController一个继承自UIViewController并设置UI.currentVCselfon viewDidAppear然后为nilon的类型viewWillDisappear。我在应用程序中的所有视图控制器都继承BaseViewController。这样,如果您有内容UI.currentVC(不是nil...)-绝对不在演示动画的中间,您可以要求它展示您的UIAlertController
Aviel Gross

1
如下所示,根视图控制器可能正在显示某些内容,在这种情况下,您的最后一个if语句失败,因此我不得不添加 else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
Niklas

27

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

但是,只要您在警报控制器中有操作,就不需要保留对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];

完美,正是我需要
解开

25

以下解决方案即使在所有版本中都看起来很有希望,也无法正常工作。此解决方案正在生成警告

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

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

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

快速的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中经过测试并且可以正常工作。


1
在加载任何根视图控制器之前,在App Delegate中针对迁移问题触发了UIAlertController的情况下,此代码对我来说非常有效。工作很好,没有警告。
邓肯·巴贝奇

3
提醒您:您需要存储对您的强烈引用,UIWindow否则该窗口将被释放,并在超出范围后不久消失。
Sirens

24

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

}

用法示例:

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无法访问,也可以使用它!
阿尔菲,

20

对于普通的视图控制器,即使在屏幕上有导航控制器,这也可以在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)

1
当我关闭警报时,UIWindow则无响应。与windowLevel可能有关。如何使其具有响应性?
滑块

1
听起来好像没有开新窗口。
伊戈尔·库拉金

看起来“窗口”没有从顶部移除,因此需要在完成操作后将其移除。
soan saini

设置你alertWindownil当你与它完成。
C6Silver

13

加上Zev的答案(然后切换回Objective-C),您可能会遇到这样的情况,即您的根视图控制器正在通过segue或其他方式呈现其他一些VC。在根VC上调用presentedViewController可以解决此问题:

[[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的方法,我们将不胜感激!

2
@Mayerz将Objective-C转换为Swift没什么大不了;),但是您在这里:UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
borchero 2015年

感谢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>)
Mojo66 '16

2
我采用了相同的方法,rootViewController.presentedViewController如果不是,则使用,否则使用rootViewController。对于完全通用的解决方案,可能有必要沿presentedViewControllers 链走去获得topmostVC
Protongun '16

9

@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似乎没有必要。
Ben Patch

请记住,苹果警告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
Grubas

6

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

4

交叉发布我的答案因为这两个线程未标记为重复对象。

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

4

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)的答案,该答案正试图以类似的方法解决相同的问题,但由于没有走上展示链,因此很容易遇到与尝试解决相同的错误。
CodeBrew

4

迅捷4+

解决方案我使用了多年没有任何问题。首先,我进行扩展UIWindow以找到它的visibleViewController。注意:如果您使用自定义collection *类(如侧面菜单),则应在以下扩展名中添加此情况的处理程序。获得最顶部的视图控制器后很容易出现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
    }
  }

}

4

对于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冻结-由于窗口仍然存在,因此无法进行交互
JBlake

如果要手动消除警报,请确保dismiss直接调用WindowAlertPresentationController alert.presentingViewController?.dismiss(animated: true, completion: nil)
JBlake

让alertController = UIAlertController(title:“ title”,消息:“ message”,preferredStyle:.alert); alertController.presentInOwnWindow(动画:false,完成:nil)对我来说很棒!谢谢!
布赖恩

这适用于装有iOS 12.4.5的iPhone 6,但不适用于装有iOS 13.3.1的iPhone 11 Pro。没有错误,但是警报从未显示。任何建议,将不胜感激。
jl303

在iOS 13上运行效果很好。在Catalyst中不起作用-消除警报后,该应用将无法进行交互。参见@Peter Lapisu的解决方案
JBlake

3

在Objective-C中显示警报的简便方法:

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

alertController你在哪UIAlertController对象。

注意:您还需要确保您的帮助程序类可以扩展 UIViewController


3

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

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:我已还原您的编辑。完全没有必要进行“错误处理”。
Martin R

2
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案例以返回nilself is UIAlertController

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

1

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

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

是的,这是可能的,并且会起作用。但是对我来说,它有点代码味道。通常,传递的参数是被调用方法执行其主要功能所必需的。另外,所有现有呼叫都需要进行修改。
Murray Sagal 2015年

1

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

到目前为止,在我的测试中似乎都可以解决。

谢谢凯文!


1

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

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


1

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

1

其中一些答案仅对我有用,将它们结合到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 ...

1

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
        }

    }

}

0

您可以尝试UIViewController使用像 - (void)presentErrorMessage;Mehtod这样的方法来实现类别,然后在该方法中实现UIAlertController,然后将其呈现在上self。比在您的客户端代码中,您将得到以下内容:

[myViewController presentErrorMessage];

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


除非我myViewController在代码中没有发生坏事的地方。那是在实用程序方法中,它对调用它的视图控制器一无所知。
Murray Sagal 2015年

2
恕我直言,向用户呈现任何视图(因此警报)是ViewControllers的责任。因此,如果代码的某些部分对viewController一无所知,则不应向用户呈现任何错误,而应将其传递给代码的“ viewController感知”部分
Vlad Soroka

2
我同意。但是,现在已弃用的便利性UIAlertView使我在几个地方违反了该规则。
Murray Sagal 2015年

0

您可以使用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 ...];

0

我在AppDelegate类中使用此代码并进行了一些个人更改

-(UIViewController*)presentingRootViewController
{
    UIViewController *vc = self.window.rootViewController;
    if ([vc isKindOfClass:[UINavigationController class]] ||
        [vc isKindOfClass:[UITabBarController class]])
    {
        // filter nav controller
        vc = [AppDelegate findChildThatIsNotNavController:vc];
        // filter tab controller
        if ([vc isKindOfClass:[UITabBarController class]]) {
            UITabBarController *tbc = ((UITabBarController*)vc);
            if ([tbc viewControllers].count > 0) {
                vc = [tbc viewControllers][tbc.selectedIndex];
                // filter nav controller again
                vc = [AppDelegate findChildThatIsNotNavController:vc];
            }
        }
    }
    return vc;
}
/**
 *   Private helper
 */
+(UIViewController*)findChildThatIsNotNavController:(UIViewController*)vc
{
    if ([vc isKindOfClass:[UINavigationController class]]) {
        if (((UINavigationController *)vc).viewControllers.count > 0) {
            vc = [((UINavigationController *)vc).viewControllers objectAtIndex:0];
        }
    }
    return vc;
}

0

似乎可以工作:

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

0

创建助手类AlertWindow,然后用作

let alertWindow = AlertWindow();
let alert = UIAlertController(title: "Hello", message: "message", preferredStyle: .alert);
let cancel = UIAlertAction(title: "Ok", style: .cancel){(action) in

    //....  action code here

    // reference to alertWindow retain it. Every action must have this at end

    alertWindow.isHidden = true;

   //  here AlertWindow.deinit{  }

}
alert.addAction(cancel);
alertWindow.present(alert, animated: true, completion: nil)


class AlertWindow:UIWindow{

    convenience init(){
        self.init(frame:UIScreen.main.bounds);
    }

    override init(frame: CGRect) {
        super.init(frame: frame);
        if let color = UIApplication.shared.delegate?.window??.tintColor {
            tintColor = color;
        }
        rootViewController = UIViewController()
        windowLevel = UIWindowLevelAlert + 1;
        makeKeyAndVisible()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    deinit{
        //  semaphor.signal();
    }

    func present(_ ctrl:UIViewController, animated:Bool, completion: (()->Void)?){
        rootViewController!.present(ctrl, animated: animated, completion: completion);
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.