问答中心分类: IOS不在视图控制器中时如何呈现 UIAlertController?
0
匿名用户 提问 7小时 前

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

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

这是可能的UIAlertView(但可能不太合适)。
在这种情况下,您如何呈现UIAlertController,就在里面myUtilityMethod?

29 Answers
0
agilityvision 回答 7小时 前

在 WWDC,我在其中一个实验室停下来,问了一位 Apple 工程师同样的问题:“展示UIAlertController?”他说他们经常收到这个问题,我们开玩笑说他们应该就这个问题进行一次会议。他说苹果内部正在创建一个UIWindow用透明的UIViewController然后呈现UIAlertController在上面。基本上迪伦·贝特曼的回答是什么。
但我不想使用的子类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被释放,因为它是唯一保留UIWindow.但是如果你分配UIAlertController通过访问其中一个操作块中的警报,到属性或导致其保留计数增加,UIWindow将留在屏幕上,锁定您的 UI。避免在需要访问的情况下查看上面的示例使用代码UITextField.
我用一个测试项目制作了一个 GitHub 存储库:FFGlobalAlertController

Dylan Bettermann 回复 7小时 前

好东西!只是一些背景知识——我使用子类而不是关联对象,因为我使用的是 Swift。关联对象是 Objective-C 运行时的一个特性,我不想依赖它。 Swift 可能还需要几年的时间才能获得它自己的运行时,但仍然如此。 🙂

Dustin Pfannenstiel 回复 7小时 前

我真的很喜欢您回答的优雅,但是我很好奇您如何淘汰新窗口并再次使原始窗口成为关键(诚然,我对窗口不太感兴趣)。

agilityvision 回复 7小时 前

@DustinPfannenstiel Alert 是唯一保留 UIWindow 的对象,因此当 Alert 被解除时,它会被解除分配,并且 UIWindow 也会随之消失。因此,为了使其工作,您不能将警报存储在属性中或通过在 UIAlertAction 块中使用警报来创建保留周期。

Dustin Pfannenstiel 回复 7小时 前

完全有可能我不明白角色的作用makeKeyAndVisible.如果新窗口成为关键窗口,那么原始窗口(来自应用程序委托)如何再次成为关键窗口?

agilityvision 回复 7小时 前

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

Dylan Bettermann 回复 7小时 前

@agilityvision 根据我的经验,这是正确的。请记住,UIWindows 也由UIWindowLevel财产。查看文档中常量的讨论部分:developer.apple.com/library/ios/documentation/UIKit/Reference/…

toofah 回复 7小时 前

我喜欢这个解决方案……它简单干净!但我似乎确实对达斯汀担心的关键窗口问题感到困惑。在我点击一个按钮关闭 UIAlertController 后,我的应用程序 ui 的其余部分似乎被阻止了。当我使用“调试视图层次结构”工具时,它肯定看起来好像没有用 windows 清理干净。

toofah 回复 7小时 前

我在 FFGlobalAlertController 示例应用程序中尝试了这个,虽然它似乎可以正常工作,但仍然存在相同类型的遗留窗口。这是显示警报之前的视图层次结构:d.pr/i/11v7d这是之后的层次结构:d.pr/i/Dp5Q

agilityvision 回复 7小时 前

这意味着您将保留 UIAlertController,而当这种情况发生时,UIWindow 不会得到释放。有关如何处理文本字段,请参阅上面的示例用法。我会用更多细节更新答案。

toofah 回复 7小时 前

谢谢,就是这样。我忘记了我正在从调用方法传回对警报的引用,以便有时可以以编程方式关闭警报。我将不得不重新考虑这一点。您是否了解为什么在您的示例代码中存在一个 UITextEffectsWindow 在您的警报关闭后出现,而以前没有?

agilityvision 回复 7小时 前

如果您希望能够以编程方式关闭警报,请使用弱引用。

toofah 回复 7小时 前

我刚刚用 Xcode 7 编译,现在我遇到了同样的问题,窗口似乎再次挂起。

toofah 回复 7小时 前

经过更多调查后,似乎为警报创建的新密钥窗口可能不会退出。在 iOS 8 中,当警报关闭时,我会收到 UIWindowDidBecomeKeyNotification,但在 iOS 9 中,该通知永远不会触发,向我表明我的警报窗口仍然存在并且仍然是关键窗口或其他东西。

toofah 回复 7小时 前

@agilityVision 对 FFGlobalAlertController 进行了修复,以解决窗口现在消失的问题。谢谢!他重写了 viewDidDisappear 方法,并在窗口外隐藏和 nil-ed。

Yoon Lee 回复 7小时 前

调用super在类别中不做任何事情。因为 Category 扩展了类,但它们没有子类。

malhal 回复 7小时 前

这是否适用于显示两个排队的警报?这是我对新课程的主要问题是重复警报刚刚被删除。

adib 回复 7小时 前

实施viewDidDisappear:在一个类别上看起来像个坏主意。从本质上讲,您正在与框架的实现竞争viewDidDisappear:.现在可能没问题,但是如果 Apple 决定将来实现该方法,那么您将无法调用它(即没有类似的super它指向类别实现中方法的主要实现)。

agilityvision 回复 7小时 前

好点,不需要 viewDidDisappear 中的代码。我后来添加了一些情况,以涵盖人们强烈引用警报并且窗口不会被删除的情况。正如我在回答中提到的。

malhal 回复 7小时 前

我已经改进了这个概念,以添加对排队多个警报的支持,如 UIAlertView 的原始行为,请参阅stackoverflow.com/a/35211571/259521

Hassy 回复 7小时 前

我替换了 [self.alertWindow makeKeyAndVisible];与 [self.alertWindow setHidden:NO];

chris 回复 7小时 前

由于覆盖,我在 iOS9.3 中遇到了这个解决方案的问题-viewDidDisappear:.调试时一切正常,但在发布版本时崩溃了。我最终创建了一个类似的解决方案子类化 UIAlertController基于此:github.com/kirbyt/WPSKit/blob/master/WPSKit/UIKit/….到目前为止,一切都很好。

Alexander Woodblock 回复 7小时 前

仅供参考,我创建了一个名为 WindowAlert 的 CocoaPod,它封装了您所描述的逻辑。github.com/DrBreen/WindowAlert

Kevin Flachsmann 回复 7小时 前

效果很好,但如何治疗prefersStatusBarHiddenpreferredStatusBarStyle没有额外的子类?

Elist 回复 7小时 前

我已经编辑了代码以不使未从主情节提要加载的应用程序崩溃,因此它们的AppDelegate可能没有window财产。

Leonid Usov 回复 7小时 前

这种方法失败了UIAlertControllerStyleActionSheet在 iPad 上。显示了操作表但无法与之交互,因为源视图和源矩形设置在当前新透明窗口下方的窗口上

Steve Moser 回复 7小时 前

可能不想使用 UIScreen 创建 UIWindow,因为如果您的应用程序处于分屏模式,您的窗口可能小于屏幕。

TealShift 回复 7小时 前

我在 iOS 10.3.1 iPhone 7 上的一位用户使用这个 hack 设法获得了不可见的按钮标签。不知道为什么。在其他设备上没有找到。很遗憾我必须从我的代码中删除它。 🙁

TealShift 回复 7小时 前

我突然想到,她的应用程序委托窗口上的色调一定很清晰。删除那部分代码可以修复它。

Gary 回复 7小时 前

如果您已经在使用启动屏幕故事板,也可能是[[UIStoryboard storyboardWithName:@"LaunchScreen" bundle: nil] instantiateInitialViewController]代替[[UIViewController alloc] init]更紧密地模仿已启动、正在运行的应用程序。

D. Mika 回复 7小时 前

我建议根本不要通过类方法显示警报。如果它现在是“可视”类,它应该将错误报告给调用者,并让被调用者决定是否值得显示警报。这样,测试也变得容易得多。

Andrew Kirna 回复 7小时 前

改用委托!

Krypt 回复 7小时 前

除非你以某种方式保留 UIAlertController-viewDidDisappear不是必需的:在对象销毁时释放所有关联对象。 UIKit 不会保留任何可见(或不可见)的 UIWindow,因此,防止 UIWindow 被破坏(并作为结果消失)的唯一引用是在关联期间创建的。结果,窗口在警报解除时被移除并销毁

coolcool1994 回复 7小时 前

此代码目前在 iOS 13 中似乎无法正常工作。就我而言,警报根本没有出现。

0
Darkngs 回答 7小时 前

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

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];
David 回复 7小时 前

+1 这是一个非常简单的解决方案。 (我面临的问题:在 Master/Detail 模板的 DetailViewController 中显示警报 – 在 iPad 上显示,从不在 iPhone 上显示)

DivideByZer0 回复 7小时 前

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

Kaptain 回复 7小时 前

Swift 3: 'Alert' 已重命名为 'alert': let alertController = UIAlertController(title: &quot;title&quot;, message: &quot;message&quot;, preferredStyle: .alert)

Andrew Kirna 回复 7小时 前

改用委托!

Kaji 回复 7小时 前

Swift 5.2:Xcode 现在说 UIApplication.shared.keyWindow 自 iOS 13.0 以来已被弃用

0
Zev Eisenberg 回答 7小时 前

您可以使用 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)
Murray Sagal 回复 7小时 前

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

Murray Sagal 回复 7小时 前

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

Zev Eisenberg 回复 7小时 前

你在哪里跑myUtilityMethod?在根视图控制器显示其视图并期望看到警报之前,您不能运行它。如果您需要在应用加载时运行它,请尝试dispatch_async(dispatch_get_main_queue(), ^{ [MyClass myUtilityMethod] });

Murray Sagal 回复 7小时 前

我是导航堆栈中的几个视图控制器。很久以前加载的应用程序。我会更新问题。

Zev Eisenberg 回复 7小时 前

您可以尝试记录导航控制器的视图窗口。

Murray Sagal 回复 7小时 前

当然,一旦我有了根视图控制器,我就可以检查viewControllers属性并拉出最上面。但也许视图控制器已被模态地推到那个控制器上。它变得尴尬。

Dylan Bettermann 回复 7小时 前

@MurraySagal 查看我的答案。如果您使用自己的 UIWindow,则不必担心哪个 UIViewController 或 UIWindow 可见。 🙂

OhadM 回复 7小时 前

使用时显示VC的好方法staticObjective C 中的方法。使用时必须使用静态方法CFNotificationCenterGetDarwinNotifyCenter.

Lubo 回复 7小时 前

@MurraySagal 有一个导航控制器,你可以得到visibleViewController属性随时查看从哪个控制器显示警报。查看文档

JAL 回复 7小时 前

@jeet.chanchawat 请不要将代码添加到其他用户的答案中。我们不想把话放在他们嘴里。如果您有不同的答案,请添加其他答案。

jeet.chanchawat 回复 7小时 前

我这样做是因为我不想把别人的工作归功于自己。这是我为 swift 3.0 修改的 @ZevEisenberg 的解决方案。如果我会添加另一个答案,那么我可能会得到他应得的投票。

Zev Eisenberg 回复 7小时 前

哦,嘿,我昨天错过了所有的戏剧,但我碰巧刚刚更新了 Swift 3 的帖子。我不知道 SO 对新语言版本更新旧答案的政策是什么,但我个人并不介意,只要答案正确!

Andrew Kirna 回复 7小时 前

改用委托!

0
Aviel Gross 回答 7小时 前

很一般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)
          }
        }
    }
}
user1585121 回复 7小时 前

我正在使用这个解决方案,我发现它非常完美、优雅、干净……但是,最近我不得不将我的根视图控制器更改为不在视图层次结构中的视图,所以这段代码变得毫无用处。任何人都在考虑使用 dix 来继续使用它吗?

Aviel Gross 回复 7小时 前

我将此解决方案与其他一些东西结合使用:我有一个单身人士UI持有(弱!)currentVC类型UIViewController。我有BaseViewController继承自UIViewController并设置UI.currentVCselfviewDidAppear然后到nilviewWillDisappear.我在应用程序中的所有视图控制器都继承BaseViewController.这样,如果你有东西在UI.currentVC(它不是nil…) – 它绝对不在演示动画的中间,您可以要求它展示您的UIAlertController.

Niklas 回复 7小时 前

如下所示,根视图控制器可能会呈现带有 segue 的内容,在这种情况下,您的最后一个 if 语句失败,所以我不得不添加else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }

0
adib 回答 7小时 前

改进敏捷视觉的答案,您需要创建一个带有透明根视图控制器的窗口,并从那里显示警报视图。
然而只要您在警报控制器中有操作,您不需要保留对窗口的引用.作为动作处理程序块的最后一步,您只需要隐藏窗口作为清理任务的一部分。通过在处理程序块中引用窗口,这会创建一个临时循环引用,一旦警报控制器被解除,该引用就会被破坏。

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];
thibaut noah 回复 7小时 前

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