ChatGPT解决这个技术问题 Extra ChatGPT

How to present UIAlertController when not in a view controller?

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?


C
Community

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


Good stuff! Just some background -- I used a subclass instead of an associated object because I was using Swift. Associated objects are a feature of the Objective-C runtime and I didn't want to be dependent on it. Swift is probably years away from getting it's own runtime, but still. :)
I really like the elegance of your answer, however I'm curious how you retire the new window and make the original window the key again (admittedly I don't muck around with the window much).
The key window is the topmost visible window, so my understanding is if you remove/hide the "key" window, the next visible window down becomes "key".
Implementing viewDidDisappear: on a category looks like a Bad Idea. In essence, you're competing with the framework's implementation of viewDidDisappear:. For now it may be okay, but if Apple decides to implement that method in the future, there's no way for it for you to call it (i.e. there is no analogous of super that points to the primary implementation of a method from a category implementation).
Works great, but how to treat prefersStatusBarHidden and preferredStatusBarStyle without an extra subclass?
D
Darkngs

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

+1 This is a brilliantly simple solution. (Problem I faced: Displaying an alert in the DetailViewController of Master/Detail template - Shows on iPad, never on iPhone)
Nice, you might want to add in another part: if (rootViewController.presentedViewController != nil) { rootViewController = rootViewController.presentedViewController; }
Swift 3: 'Alert' has been renamed to 'alert': let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert)
Use a delegate instead!
Swift 5.2: Xcode is now saying that UIApplication.shared.keyWindow has been deprecated since iOS 13.0
Z
Zev Eisenberg

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)

Oops, I accepted before I checked. That code returns the root view controller, which in my case is the navigation controller. It doesn't cause an error but the alert doesn't display.
And I noticed in the console: Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.
@MurraySagal having a navigation controller you can get the visibleViewController property at any time to see what controller to present the alert from. Check out the docs
I did it because I don't want to take credits of someone else's work. It was @ZevEisenberg 's solution which I modified for swift 3.0 . If I would have added another answer then I might have got vote ups which he deserves.
Oh hey, I missed all the drama yesterday, but I happen to have just updated the post for Swift 3. I don't know what SO's policy is on updating old answers for new language versions, but I personally don't mind it, as long as the answer is correct!
C
Community

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


You don't have this for Objective-C, do you?
Yes, it even works in Swift 2.0/iOS 9. I'm working on an Objective-C version right now because someone else asked for it (maybe it was you). I'll post back when I'm done.
G
Gerald

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

I was using this solution, and I found it really perfect, elegant, clean... BUT, recently I had to change my root view controller to a view not in the view hierarchy, so this code became useless. Anyone thinking of a dix for keeping using this?
I use a combination of this solution with sometinhg else: I have a singleton UI class which holds a (weak!) currentVC of type UIViewController.I have BaseViewController which inherits from UIViewController and set UI.currentVC to self on viewDidAppear then to nil on viewWillDisappear. All my view controllers in the app inherit BaseViewController. That way if you have something in UI.currentVC (it's not nil...) - it is definetly not in the middle of a presentation animation, and you can ask it to present your UIAlertController.
As per below, the root view controller might be presenting something with a segue, in which case your last if statement fails, so I had to add else { if let presentedViewController = controller.presentedViewController { presentedViewController.presentViewController(self, animated: animated, completion: completion) } else { controller.presentViewController(self, animated: animated, completion: completion) } }
C
Community

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

Perfect, exactly the tip i needed to dismiss the window, thanks mate
C
Community

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.


This code worked perfectly for me, in a context where a UIAlertController was being fired off in the App Delegate regarding a migration issue, before any root view controller had been loaded. Worked great, no warnings.
Just a reminder: you need to store a strong reference to your UIWindow or else the window will be released and will disappear shortly after going out of scope.
k
konekoya

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

This can be used even if sharedApplication is not accessible !
I get the error: Unbalanced calls to begin/end appearance transitions for
The alert automatically dismissed itself? Not exactly sure why it's doing that
E
ElOjcar

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)

When I dismiss the alert, the UIWindow is nonresponsive. Something to do with the windowLevel probably. How can I make it responsive?
Sounds like new window wasn't dismissed.
Looks like Window does not get Removed from top, So need to remove the window once done.
Set your alertWindow to nil when you are finished with it.
K
Kevin Sliech

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.


Hum I am running into this problem in Swift, and I dont find how to translate your objc code to swift, help would be much appreciated!
@Mayerz translating Objective-C to Swift shouldn't be such a big deal ;) but here you are: UIApplication.sharedApplication().keyWindow?.rootViewController?.presentedViewController?.presentViewController(controller, animated: true, completion: nil)
Thanks Olivier, you are right, it's easy as pie, and I did translated it this way, but the problem was lying somewhere else. Thanks anyway!
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)
I went with the same approach, use the rootViewController.presentedViewController if its not nil, otherwise using rootViewController. For a fully generic solution, it may be necessary to walk the chain of presentedViewControllers to get at the topmost VC
D
Dylan Colaco

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

I was getting a black background with the accepted answer. window.backgroundColor = UIColor.clear fixed that. viewController.view.backgroundColor = UIColor.clear doesn't appear to be necessary.
Keep in mind that Apple warns about UIAlertController subclassing: The UIAlertController class is intended to be used as-is and does not support subclassing. The view hierarchy for this class is private and must not be modified. developer.apple.com/documentation/uikit/uialertcontroller
L
Logan Gauthier

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

This doesn't work if you need to dismiss the alert manually - the WindowAlertPresentationController is never de-allocated, resulting in a frozen UI - nothing is interactive due to the window still there
If you want to dismiss the alert manually, make sure to call dismiss on the WindowAlertPresentationController directly alert.presentingViewController?.dismiss(animated: true, completion: nil)
let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert); alertController.presentInOwnWindow(animated: false, completion: nil) works great for me! Thanks!
This works on iPhone 6 with iOS 12.4.5, but not on iPhone 11 Pro with iOS 13.3.1. There's no error, but the alert never gets displayed. Any suggestion would be appreciated.
Works great for iOS 13. Does not work in Catalyst -- once the alert is dismissed, the app is not interact-able. See @Peter Lapisu's solution
e
exp

Swift 5

It's important to hide the window after showing the message.

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

Will hiding the alertWindow remove the UIViewController you have created? I think it's better to dismiss the rootViewController like this alertWindow.rootViewController?.dismiss(animated: false)
This is the best one, but in the close handler you should alertWindow.removeFromSuperview() not let them keep adding a new UIWindow() every time the root function is called.
M
Marcin Kapusta

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

T
Timur Bernikovich

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

}

C
Community

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

C
CodeBrew

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.


Just went through the many previous answers here and saw Kevin Sliech's answer, which is trying to solve the same issue with a similar approach but which stopped short of walking up the presentation chain, thus making it susceptible to the same error as it tries to solve.
E
Eerko

Some of these answers only worked partly for me, combining them in the following class method in AppDelegate was the solution for me. It works on iPad, in UITabBarController views, in UINavigationController, en when presenting modals. Tested on iOS 10 and 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;
}

Usage:

[[AppDelegate rootViewController] presentViewController ...

V
ViperMav

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


M
Martin R

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

@Chathuranga: I have reverted your edit. That “error handling” is completely unnecessary.
P
Peter Lapisu

iOS13 scene support (when using UIWindowScene)

import UIKit

private var windows: [String:UIWindow] = [:]

extension UIWindowScene {
    static var focused: UIWindowScene? {
        return UIApplication.shared.connectedScenes
            .first { $0.activationState == .foregroundActive && $0 is UIWindowScene } as? UIWindowScene
    }
}

class StyledAlertController: UIAlertController {

    var wid: String?

    func present(animated: Bool, completion: (() -> Void)?) {

        //let window = UIWindow(frame: UIScreen.main.bounds)
        guard let window = UIWindowScene.focused.map(UIWindow.init(windowScene:)) else {
            return
        }
        window.rootViewController = UIViewController()
        window.windowLevel = .alert + 1
        window.makeKeyAndVisible()
        window.rootViewController!.present(self, animated: animated, completion: completion)

        wid = UUID().uuidString
        windows[wid!] = window
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        if let wid = wid {
            windows[wid] = nil
        }

    }

}

UIAlerController shouldn't be subclassed according to documentation developer.apple.com/documentation/uikit/uialertcontroller
UIButton shouldn't also be sub-classed and everybody does... as long as you don't modify private methods and don't change the views inside the alertcontroller, its ok
I couldn't find any reference to UIButton shouldn't also be sub-classed. Can you please share the source?
Thank you! Having UIScenes really messed with how to create the window, and your approach to that is nice.
D
December

Updated to work with iOS 13 Scenes which breaks the new UIWindow approach. Swift 5.1.

fileprivate var alertWindows = [UIAlertController:UIWindow]()

extension UIAlertController {

    func presentInNewWindow(animated: Bool, completion: (() -> Void)?) {
        let foregroundActiveScene = UIApplication.shared.connectedScenes.filter { $0.activationState == .foregroundActive }.first
        guard let foregroundWindowScene = foregroundActiveScene as? UIWindowScene else { return }

        let window = UIWindow(windowScene: foregroundWindowScene)
        alertWindows[self] = window

        window.rootViewController = UIViewController()
        window.windowLevel = .alert + 1
        window.makeKeyAndVisible()
        window.rootViewController!.present( self, animated: animated, completion: completion)
    }

    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        alertWindows[self] = nil
    }

}

viewDidDisappear() isn't getting called after dismissing the alert. Does anyone know why? The apparent result is that the app is frozen
c
computingfreak

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!


N
NSExceptional
extension UIApplication {
    /// The top most view controller
    static var topMostViewController: UIViewController? {
        return UIApplication.shared.keyWindow?.rootViewController?.visibleViewController
    }
}

extension UIViewController {
    /// The visible view controller from a given view controller
    var visibleViewController: UIViewController? {
        if let navigationController = self as? UINavigationController {
            return navigationController.topViewController?.visibleViewController
        } else if let tabBarController = self as? UITabBarController {
            return tabBarController.selectedViewController?.visibleViewController
        } else if let presentedViewController = presentedViewController {
            return presentedViewController.visibleViewController
        } else {
            return self
        }
    }
}

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

P
Pablo A.

You can send the current view or controller as a parameter:

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

Yes, that's possible and would work. But for me, it's got a bit of a code smell. Parameters passed should generally be required for the called method to perform it's primary function. Plus all the existing calls would need to be modified.
C
Community

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.


w
wonder.mice

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

This is actually ingenious. May not be ideal in all scenarios, especially if you're using a child view controller that should ideally not be presenting a view controller, but this can be improved upon.
A
AaoIi

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

M
Mike Koene

Another option:

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

G
Gralex

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

关注公众号,不定期副业成功案例分享
Follow WeChat

Success story sharing

Want to stay one step ahead of the latest teleworks?

Subscribe Now