场景:用户点击视图控制器上的按钮。视图控制器是导航堆栈中的最顶层(显然)。点击调用在另一个类上调用的实用程序类方法。那里发生了一件坏事,我想在控制返回视图控制器之前在那里显示一个警报。
+ (void)myUtilityMethod {
// do stuff
// something bad happened, display an alert.
}
UIAlertView
可以做到这一点(但可能不太合适)。
在这种情况下,您如何在 myUtilityMethod
中显示一个 UIAlertController
?
在 WWDC,我在其中一个实验室停下来,问了一位 Apple 工程师同样的问题:“显示 UIAlertController
的最佳做法是什么?”他说他们经常收到这个问题,我们开玩笑说他们应该就这个问题进行一次会议。他说,Apple 内部正在创建一个带有透明 UIViewController
的 UIWindow
,然后在其上显示 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
迅速
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];
您可以使用 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!
。
visibleViewController
属性以查看从哪个控制器显示警报。查看the docs
几个月前我发布了一个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
对于 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)
}
}
}
}
UI
类,它包含一个(弱!)currentVC
类型为 UIViewController
。我有 BaseViewController
,它继承自 UIViewController
并设置 {6 } 在 viewDidAppear
上到 self
,然后在 viewWillDisappear
上到 nil
。我在应用程序中的所有视图控制器都继承 BaseViewController
。这样,如果您在 UI.currentVC
中有东西(它不是 nil
...) - 它肯定不在演示动画的中间,您可以要求它展示您的 UIAlertController
。
else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
在 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];
以下解决方案没有工作,尽管它在所有版本中看起来都非常有希望。 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 中经过测试和工作的代码。
UIWindow
的强引用,否则窗口将被释放并在超出范围后很快消失。
这是 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")
})
这在 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
。
添加到 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 恰好是一个导航控制器,这也可能是必要的。
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)
rootViewController.presentedViewController
,否则使用 rootViewController
。对于完全通用的解决方案,可能需要遍历 presentedViewController
链才能到达 topmost
VC
@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
似乎没有必要。
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
对于 iOS 13,基于 mythicalcoder 和 bobbyrehm 的答案:
在 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)
}
}
dismiss
alert.presentingViewController?.dismiss(animated: true, completion: nil)
斯威夫特 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.rootViewController?.dismiss(animated: false)
alertWindow.removeFromSuperview()
不要让他们在每次调用根函数时都继续添加新的 UIWindow()。
像 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+
我使用多年的解决方案完全没有问题。首先我扩展 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
}
}
}
交叉发布我的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)
}
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 种子中运行良好,具有正确的色调,并且没有键盘滑动伪影。
其中一些答案仅对我部分有效,将它们组合在 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 ...
在 Objective-C 中显示警报的速记方法:
[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];
其中 alertController
是您的 UIAlertController
对象。
注意:您还需要确保您的助手类扩展 UIViewController
如果有人感兴趣,我创建了一个 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
}
}
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
}
}
}
UIButton shouldn't also be sub-classed
的任何引用。请问可以分享出处吗?
更新为与 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
}
}
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;
}
到目前为止,在我的测试中似乎一切正常。
谢谢凯文!
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
}
}
}
您可以将当前视图或控制器作为参数发送:
+ (void)myUtilityMethod:(id)controller {
// do stuff
// something bad happened, display an alert.
}
除了给出很好的答案(agilityvision、adib、malhal)。要达到类似旧 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];
}];
这将允许您避免警报窗口重叠。可以使用相同的方法为任意数量的窗口层分离和放入队列视图控制器。
似乎工作:
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];
}
我尝试了所有提到的,但没有成功。我用于 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)
}
}
}
另外的选择:
var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
while ((topController.presentedViewController) != nil) {
topController = topController.presentedViewController!
}
topController.present(alert, animated:true, completion:nil)
您可以使用 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 ...];
不定期副业成功案例分享
viewDidDisappear:
似乎是个坏主意。实质上,您是在与框架的viewDidDisappear:
实现竞争。现在可能没问题,但是如果 Apple 决定在未来实现该方法,那么您将无法调用它(即没有类似的super
指向一个方法的主要实现类实现)。prefersStatusBarHidden
和preferredStatusBarStyle
?