问题
Scenario: The user taps on a button on a view controller. The view controller is the topmost (obviously) in the navigation stack. The tap invokes a utility class method called on another class. A bad thing happens there and I want to display an alert right there before control returns to the view controller.
+ (void)myUtilityMethod {
// do stuff
// something bad happened, display an alert.
}
This was possible with UIAlertView
(but perhaps not quite proper).
In this case, how do you present a UIAlertController
, right there in myUtilityMethod
?
回答1:
I posted a similar question a couple months ago and think I've finally solved the problem. Follow the link at the bottom of my post if you just want to see the code.
The solution is to use an additional UIWindow.
When you want to display your UIAlertController:
- Make your window the key and visible window (
window.makeKeyAndVisible()
) - Just use a plain UIViewController instance as the rootViewController of the new window. (
window.rootViewController = UIViewController()
) - Present your UIAlertController on your window's rootViewController
A couple things to note:
- Your UIWindow must be strongly referenced. If it's not strongly referenced it will never appear (because it is released). I recommend using a property, but I've also had success with an associated object.
- To ensure that the window appears above everything else (including system UIAlertControllers), I set the windowLevel. (
window.windowLevel = UIWindowLevelAlert + 1
)
Lastly, I have a completed implementation if you just want to look at that.
https://github.com/dbettermann/DBAlertController
回答2:
At WWDC, I stopped in at one of the labs and asked an Apple Engineer this same question: "What was the best practice for displaying a UIAlertController
?" And he said they had been getting this question a lot and we joked that they should have had a session on it. He said that internally Apple is creating a UIWindow
with a transparent UIViewController
and then presenting the UIAlertController
on it. Basically what is in Dylan Betterman's answer.
But I didn't want to use a subclass of UIAlertController
because that would require me changing my code throughout my app. So with the help of an associated object, I made a category on UIAlertController
that provides a show
method in Objective-C.
Here is the relevant code:
#import "UIAlertController+Window.h"
#import <objc/runtime.h>
@interface UIAlertController (Window)
- (void)show;
- (void)show:(BOOL)animated;
@end
@interface UIAlertController (Private)
@property (nonatomic, strong) UIWindow *alertWindow;
@end
@implementation UIAlertController (Private)
@dynamic alertWindow;
- (void)setAlertWindow:(UIWindow *)alertWindow {
objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIWindow *)alertWindow {
return objc_getAssociatedObject(self, @selector(alertWindow));
}
@end
@implementation UIAlertController (Window)
- (void)show {
[self show:YES];
}
- (void)show:(BOOL)animated {
self.alertWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
self.alertWindow.rootViewController = [[UIViewController alloc] init];
id<UIApplicationDelegate> delegate = [UIApplication sharedApplication].delegate;
// Applications that does not load with UIMainStoryboardFile might not have a window property:
if ([delegate respondsToSelector:@selector(window)]) {
// we inherit the main window's tintColor
self.alertWindow.tintColor = delegate.window.tintColor;
}
// window level is above the top window (this makes the alert, if it's a sheet, show over the keyboard)
UIWindow *topWindow = [UIApplication sharedApplication].windows.lastObject;
self.alertWindow.windowLevel = topWindow.windowLevel + 1;
[self.alertWindow makeKeyAndVisible];
[self.alertWindow.rootViewController presentViewController:self animated:animated completion:nil];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// precaution to ensure window gets destroyed
self.alertWindow.hidden = YES;
self.alertWindow = nil;
}
@end
Here is a sample usage:
// need local variable for TextField to prevent retain cycle of Alert otherwise UIWindow
// would not disappear after the Alert was dismissed
__block UITextField *localTextField;
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"Global Alert" message:@"Enter some text" preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSLog(@"do something with text:%@", localTextField.text);
// do NOT use alert.textfields or otherwise reference the alert in the block. Will cause retain cycle
}]];
[alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
localTextField = textField;
}];
[alert show];
The UIWindow
that is created will be destroyed when the UIAlertController
is dealloced, since it is the only object that is retaining the UIWindow
. But if you assign the UIAlertController
to a property or cause its retain count to increase by accessing the alert in one of the action blocks, the UIWindow
will stay on screen, locking up your UI. See the sample usage code above to avoid in the case of needing to access UITextField
.
I made a GitHub repo with a test project: FFGlobalAlertController
回答3:
You can do the following with Swift 2.2:
let alertController: UIAlertController = ...
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(alertController, animated: true, completion: nil)
And Swift 3.0:
let alertController: UIAlertController = ...
UIApplication.shared.keyWindow?.rootViewController?.present(alertController, animated: true, completion: nil)
回答4:
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];
回答5:
Pretty generic UIAlertController
extension
for all cases of UINavigationController
and/or UITabBarController
. Also works if there's a modal VC on screen at the moment.
Usage:
//option 1:
myAlertController.show()
//option 2:
myAlertController.present(animated: true) {
//completion code...
}
This is the extension:
//Uses Swift1.2 syntax with the new if-let
// so it won't compile on a lower version.
extension UIAlertController {
func show() {
present(animated: true, completion: nil)
}
func present(#animated: Bool, completion: (() -> Void)?) {
if let rootVC = UIApplication.sharedApplication().keyWindow?.rootViewController {
presentFromController(rootVC, animated: animated, completion: completion)
}
}
private func presentFromController(controller: UIViewController, animated: Bool, completion: (() -> Void)?) {
if let navVC = controller as? UINavigationController,
let visibleVC = navVC.visibleViewController {
presentFromController(visibleVC, animated: animated, completion: completion)
} else {
if let tabVC = controller as? UITabBarController,
let selectedVC = tabVC.selectedViewController {
presentFromController(selectedVC, animated: animated, completion: completion)
} else {
controller.presentViewController(self, animated: animated, completion: completion)
}
}
}
}
回答6:
Improving on agilityvision's answer, you'll need to create a window with a transparent root view controller and present the alert view from there.
However as long as you have an action in your alert controller, you don't need to keep a reference to the window. As a final step of the action handler block, you just need to hide the window as part of the cleanup task. By having a reference to the window in the handler block, this creates a temporary circular reference that would be broken once the alert controller is dismissed.
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];
回答7:
The following solution did not work even though it looked quite promising with all the versions. This solution is generating WARNING.
Warning: Attempt to present on whose view is not in the window hierarchy!
https://stackoverflow.com/a/34487871/2369867 =>
This is looked promising then. But it was not in Swift 3
.
So I am answering this in Swift 3 and this is not template example.
This is rather fully functional code by itself once you paste inside any function.
Quick
Swift 3
self-contained code
let alertController = UIAlertController(title: "<your title>", message: "<your message>", preferredStyle: UIAlertControllerStyle.alert)
alertController.addAction(UIAlertAction(title: "Close", style: UIAlertActionStyle.cancel, handler: nil))
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alertController, animated: true, completion: nil)
This is tested and working code in Swift 3.
回答8:
Here's mythicalcoder's answer as an extension, tested & working in Swift 4:
extension UIAlertController {
func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1;
alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(self, animated: animated, completion: completion)
}
}
Example usage:
let alertController = UIAlertController(title: "<Alert Title>", message: "<Alert Message>", preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "Close", style: .cancel, handler: nil))
alertController.presentInOwnWindow(animated: true, completion: {
print("completed")
})
回答9:
This works in Swift for normal view controllers and even if there is a navigation controller on the screen:
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)
回答10:
Adding on to Zev's answer (and switching back to Objective-C), you could run into a situation where your root view controller is presenting some other VC via a segue or something else. Calling presentedViewController on the root VC will take care of this:
[[UIApplication sharedApplication].keyWindow.rootViewController.presentedViewController presentViewController:alertController animated:YES completion:^{}];
This straightened out an issue I had where the root VC had segued to another VC, and instead of presenting the alert controller, a warning like those reported above was issued:
Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!
I haven't tested it, but this may also be necessary if your root VC happens to be a navigation controller.
回答11:
@agilityvision's answer translated to Swift4/iOS11. I haven't used localized strings, but you can change that easily:
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)
}
}
回答12:
Create Extension like in Aviel Gross answer. Here You have Objective-C extension.
Here You have header file *.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
And implementation: *.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
You are using this extension in Your implementation file like this:
#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];
回答13:
Cross post my answer since these two threads are not flagged as dupes...
Now that UIViewController
is part of the responder chain, you can do something like this:
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)
}
回答14:
Zev Eisenberg's answer is simple and straightforward, but it does not always work, and it may fail with this warning message:
Warning: Attempt to present <UIAlertController: 0x7fe6fd951e10>
on <ThisViewController: 0x7fe6fb409480> which is already presenting
<AnotherViewController: 0x7fe6fd109c00>
This is because the windows rootViewController is not at the top of the presented views. To correct this we need to walk up the presentation chain, as shown in my UIAlertController extension code written in Swift 3:
/// 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)
}
Updates on 9/15/2017:
Tested and confirmed that the above logic still works great in the newly available iOS 11 GM seed. The top voted method by agilityvision, however, does not: the alert view presented in a newly minted UIWindow
is below the keyboard and potentially prevents the user from tapping its buttons. This is because in iOS 11 all windowLevels higher than that of keyboard window is lowered to a level below it.
One artifact of presenting from keyWindow
though is the animation of keyboard sliding down when alert is presented, and sliding up again when alert is dismissed. If you want the keyboard to stay there during presentation, you can try to present from the top window itself, as shown in below code:
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
}
}
}
The only not so great part of the above code is that it checks the class name UIRemoteKeyboardWindow
to make sure we can include it too. Nevertheless the above code does work great in iOS 9, 10 and 11 GM seed, with the right tint color and without the keyboard sliding artifacts.
回答15:
Swift 4+
Solution I use for years with no issues at all. First of all I extend UIWindow
to find it's visibleViewController. NOTE: if you using custom collection* classes (such as side menu) you should add handler for this case in following extension. After getting top most view controller it's easy to present UIAlertController
just like 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
}
}
}
回答16:
Shorthand way to do present the alert in Objective-C:
[[[[UIApplication sharedApplication] keyWindow] rootViewController] presentViewController:alertController animated:YES completion:nil];
Where alertController
is your UIAlertController
object.
NOTE: You'll also need to make sure your helper class extends UIViewController
回答17:
If anyone is interested I created a Swift 3 version of @agilityvision answer. The code:
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
}
}
回答18:
For iOS 13, building on the answers by mythicalcoder and bobbyrehm:
In iOS 13, if you are creating your own window to present the alert in, you are required to hold a strong reference to that window or else your alert won't be displayed because the window will be immediately deallocated when its reference exits scope.
Furthermore, you'll need to set the reference to nil again after the alert is dismissed in order to remove the window to continue to allow user interaction on the main window below it.
You can create a UIViewController
subclass to encapsulate the window memory management logic:
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?()
}
}
}
You can use this as is, or if you want a convenience method on your UIAlertController
, you can throw it in an extension:
extension UIAlertController {
func presentInOwnWindow(animated: Bool, completion: (() -> Void)?) {
let windowAlertPresentationController = WindowAlertPresentationController(alert: self)
windowAlertPresentationController.present(animated: animated, completion: completion)
}
}
回答19:
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
}
}
}
With this you can easily present your alert like so
UIApplication.topMostViewController?.present(viewController, animated: true, completion: nil)
One thing to note is that if there's a UIAlertController currently being displayed, UIApplication.topMostViewController
will return a UIAlertController
. Presenting on top of a UIAlertController
has weird behavior and should be avoided. As such, you should either manually check that !(UIApplication.topMostViewController is UIAlertController)
before presenting, or add an else if
case to return nil if self 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
}
}
}
回答20:
You can send the current view or controller as a parameter:
+ (void)myUtilityMethod:(id)controller {
// do stuff
// something bad happened, display an alert.
}
回答21:
Kevin Sliech provided a great solution.
I now use the below code in my main UIViewController subclass.
One small alteration i made was to check to see if the best presentation controller is not a plain UIViewController. If not, it's got to be some VC that presents a plain VC. Thus we return the VC that's being presented instead.
- (UIViewController *)bestPresentationController
{
UIViewController *bestPresentationController = [UIApplication sharedApplication].keyWindow.rootViewController;
if (![bestPresentationController isMemberOfClass:[UIViewController class]])
{
bestPresentationController = bestPresentationController.presentedViewController;
}
return bestPresentationController;
}
Seems to all work out so far in my testing.
Thank you Kevin!
回答22:
In addition to great answers given (agilityvision, adib, malhal). To reach queueing behaviour like in good old UIAlertViews (avoid alert windows overlap), use this block to observe window level availability:
@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
Complete example:
[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];
}];
This will allow you to avoid alert windows overlap. Same method can be used to separate and put in queue view controllers for any number of window layers.
回答23:
I tried everything mentioned, but with no success. The method which I used for 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)
}
}
}
回答24:
You can try to implement a category on UIViewController
with mehtod like
- (void)presentErrorMessage;
And and inside that method you implement UIAlertController and then present it on self
. Than in your client code you will have something like:
[myViewController presentErrorMessage];
In that way you'll avoid unneccessary parametrs and warnings about view not being in window hierarchy.
回答25:
There 2 approaches that you can use:
-Use UIAlertView
or 'UIActionSheet' instead (not recommended, cause it deprecated in iOS 8 but it works now)
-Somehow remember the last view controller which is presented. Here is example.
@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
Usage:
[[UIViewController topViewController] presentViewController:alertController ...];
回答26:
I use this code with some little personal variations in my AppDelegate class
-(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;
}
回答27:
Seems to work:
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];
}
回答28:
create helper class AlertWindow and than use as
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);
}
}
回答29:
@agilityvision's answer is so good. I have sense used in swift projects so I thought I would share my take on his answer using swift 3.0
fileprivate class MyUIAlertController: UIAlertController {
typealias Handler = () -> Void
struct AssociatedKeys {
static var alertWindowKey = "alertWindowKey"
}
dynamic var _alertWindow: UIWindow?
var alertWindow: UIWindow? {
return objc_getAssociatedObject(self, &AssociatedKeys.alertWindowKey) as? UIWindow
}
func setAlert(inWindow window: UIWindow) {
objc_setAssociatedObject(self, &AssociatedKeys.alertWindowKey, _alertWindow, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
func show(completion: Handler? = nil) {
show(animated: true, completion: completion)
}
func show(animated: Bool, completion: Handler? = nil) {
_alertWindow = UIWindow(frame: UIScreen.main.bounds)
_alertWindow?.rootViewController = UIViewController()
if let delegate: UIApplicationDelegate = UIApplication.shared.delegate, let window = delegate.window {
_alertWindow?.tintColor = window?.tintColor
}
let topWindow = UIApplication.shared.windows.last
_alertWindow?.windowLevel = topWindow?.windowLevel ?? 0 + 1
_alertWindow?.makeKeyAndVisible()
_alertWindow?.rootViewController?.present(self, animated: animated, completion: completion)
}
fileprivate override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
_alertWindow?.isHidden = true
_alertWindow = nil
}
}
回答30:
In Swift 3
let alertLogin = UIAlertController.init(title: "Your Title", message:"Your message", preferredStyle: .alert)
alertLogin.addAction(UIAlertAction(title: "Done", style:.default, handler: { (AlertAction) in
}))
self.window?.rootViewController?.present(alertLogin, animated: true, completion: nil)
来源:https://stackoverflow.com/questions/51884294/attempt-to-present-uialertcontroller-on-uiviewcontroller-whose-view-is-not-i