How to present UIAlertController when not in a view controller?
Asked Answered
D

39

294

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?

Diazotize answered 24/10, 2014 at 19:24 Comment(0)
C
46

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:

  1. Make your window the key and visible window (window.makeKeyAndVisible())
  2. Just use a plain UIViewController instance as the rootViewController of the new window. (window.rootViewController = UIViewController())
  3. 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

Crossfertilization answered 28/5, 2015 at 13:39 Comment(2)
You don't have this for Objective-C, do you?Ineffaceable
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.Crossfertilization
M
342

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

Mitchel answered 19/6, 2015 at 15:0 Comment(33)
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. :)Crossfertilization
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).Dolabriform
@DustinPfannenstiel The Alert is the only object that has retained the UIWindow, so when the Alert is dismissed, it then gets dealloced and along with it the UIWindow and so it disappears. So in order for this to work you can't store the Alert in a property or create a retain cycle by using Alert in a UIAlertAction block.Mitchel
It's entirely possible I don't understand the role of makeKeyAndVisible. If the new window becomes the key window, how does the original window (from the app delegate) become the key window again?Dolabriform
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".Mitchel
@Mitchel In my experience that's correct. Do keep in mind that UIWindows are also ordered by the UIWindowLevel property. Take a look at the Discussion section for the constant in the documentation: developer.apple.com/library/ios/documentation/UIKit/Reference/…Crossfertilization
I love this solution...it's simple and clean! But I do seem to be having trouble with the key window stuff that Dustin was worried about. After I tap a button to close out the UIAlertController the rest of my app ui appears to be blocked. When I use the 'debug view hierarchy' tool it definitely looks like something is not quite cleaned up with the windows.Reliance
I tried this in the FFGlobalAlertController example app and although it appears to work correctly the same sort of left over windows are there. Here is the view hierarchy before the alert is shown:d.pr/i/11v7d and here is the hierarchy after:d.pr/i/Dp5QReliance
That means you are retaining the UIAlertController, and when that happens the UIWindow doesn't get dealloc. See sample usage above for how to handle textfields. I'll update answer with more details.Mitchel
Thanks, that was it. I forgot that I was passing back a reference to my alert from the calling method so that I can sometimes programmatically close the alert. I'll have to rethink that. Do you understand why in your example code there is a UITextEffectsWindow present after your alert closes that was no there previously?Reliance
use a weak reference if you want to be able programmatically close the alert.Mitchel
I just compiled with Xcode 7 and now my I am experiencing the same problem where the window seems to be hanging around again.Reliance
After some more investigation it seems that the new key window created for the alert might not be resigning. In iOS 8 I get UIWindowDidBecomeKeyNotification when the alert closes, but in iOS 9 that notification never fires, signaling to me that my alert window is still around and is still the key window or something.Reliance
@agilityVision made a fix to FFGlobalAlertController to address the issue of the window now going away. Thanks! He overwrote for the viewDidDisappear method and hid and nil-ed out the window.Reliance
Calling the super doesn't do anything in Category. Because Category extends the class, but they don't subclass.Cancel
Does this work for showing two alerts that queue up? That's my main issue with the new class is duplicate alerts just get dropped.Discobolus
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).Hawaiian
good point, the code in viewDidDisappear is not needed. I added in later after to cover cases where people had a strong reference to the alert and the window wouldn't then get delloced. As I mentioned in my answer.Mitchel
I've improved on this concept to add support for queueing up multiple alerts like the original behaviour of UIAlertView, see https://mcmap.net/q/102057/-concurrent-uialertcontrollersDiscobolus
I replaced the line [self.alertWindow makeKeyAndVisible]; with [self.alertWindow setHidden:NO];Lyndseylyndsie
I had problems with this solution in iOS9.3 due to the override of -viewDidDisappear:. Things worked fine when debugging but crashed with a release build. I ended up creating a similar solution that subclassed UIAlertController based on this: github.com/kirbyt/WPSKit/blob/master/WPSKit/UIKit/…. So far so good.Acuff
FYI, I've created a CocoaPod called WindowAlert that encapsulates the logic you've described. github.com/DrBreen/WindowAlertMallett
Works great, but how to treat prefersStatusBarHidden and preferredStatusBarStyle without an extra subclass?Cryan
I have edited the code to not crash applications that does not load from a main storyboard, hence their AppDelegate might not have the window property.Progesterone
This approach fails for UIAlertControllerStyleActionSheet on iPad. The action sheet is presented but no way to interact with it, since the source view and source rect are set on the window below the currently new transparent windowDonegan
Might not want to create your UIWindow with UIScreen since your window might be smaller than your screen if your app is in split screen mode.Phidias
One of my users on an iOS 10.3.1 iPhone 7 managed to get invisible button labels using this hack. No idea why. It's not been found on other devices. Shame I have to remove this from my code. :(Cerell
It just occurred to me that the tint color must have been clear on her app delegate's window somehow. Removing that part of the code fixes it.Cerell
If you are already using a launch screen storyboard, could also be [[UIStoryboard storyboardWithName:@"LaunchScreen" bundle: nil] instantiateInitialViewController] instead of [[UIViewController alloc] init] to more closely imitate a launched, running app.Pesthouse
I would recommend not presenting the alert by the class method at all. If it's a now 'visual' class, it should report the error back to the caller and let the called decide wether it's worth showing an alert or not. This way it becomes a lot easier to test as well.Oeildeboeuf
Use a delegate instead!Molluscoid
Unless you retaining UIAlertController somewhere in some way -viewDidDisappear is not required: all associated objects are released at moment of object destruction. Any visible (or invisible) UIWindow are not retained by UIKit, so, the only reference preventing UIWindow from destruction (and disappearing as result) is created during association. As result, window got removed and destroyed at moment of alert dismissalUnmoor
This code doesn't seem to be working right now in iOS 13. Alert doesn't get presented at all in my case.Milliemillieme
M
128

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];
Marilee answered 4/11, 2014 at 13:10 Comment(5)
+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)Siqueiros
Nice, you might want to add in another part: if (rootViewController.presentedViewController != nil) { rootViewController = rootViewController.presentedViewController; }Misogyny
Swift 3: 'Alert' has been renamed to 'alert': let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert)Tamarin
Use a delegate instead!Molluscoid
Swift 5.2: Xcode is now saying that UIApplication.shared.keyWindow has been deprecated since iOS 13.0Kook
E
113

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)
Epigeal answered 24/10, 2014 at 19:55 Comment(13)
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.Diazotize
And I noticed in the console: Warning: Attempt to present <UIAlertController: 0x145bfa30> on <UINavigationController: 0x1458e450> whose view is not in the window hierarchy!.Diazotize
Where are you running myUtilityMethod? You can’t run it before the root view controller has displayed its view and expect to see an alert. If you need to run it when the app loads, try dispatch_async(dispatch_get_main_queue(), ^{ [MyClass myUtilityMethod] });Epigeal
I'm several view controllers into the navigation stack. The app loaded long ago. I'll update the question.Diazotize
You could try logging the navigation controller's view's window.Epigeal
Sure, once I have the root view controller I could inspect the viewControllers property and pull out the topmost. But maybe a view controller has been pushed modally on that one. It gets awkward.Diazotize
@MurraySagal check out my answer. You don't have to worry about which UIViewController or UIWindow is visible if you use your own UIWindow. :)Crossfertilization
It's a good method to display a VC when you're using static method in Objective C. Static method is mandatory when using CFNotificationCenterGetDarwinNotifyCenter.Beccafico
@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 docsCorbicula
@jeet.chanchawat Please do not add code to another user's answer. We don't want to put words in their mouth. If you have a different answer, please add an additional answer.Dialectics
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.Martelli
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!Epigeal
Use a delegate instead!Molluscoid
C
46

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:

  1. Make your window the key and visible window (window.makeKeyAndVisible())
  2. Just use a plain UIViewController instance as the rootViewController of the new window. (window.rootViewController = UIViewController())
  3. 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

Crossfertilization answered 28/5, 2015 at 13:39 Comment(2)
You don't have this for Objective-C, do you?Ineffaceable
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.Crossfertilization
R
36

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)
          }
        }
    }
}
Roster answered 23/4, 2015 at 12:3 Comment(3)
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?Riana
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.Roster
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) } }Anklebone
H
32

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];
Hawaiian answered 23/1, 2016 at 6:59 Comment(1)
Perfect, exactly the tip i needed to dismiss the window, thanks mateHarappa
F
26

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://mcmap.net/q/100026/-how-to-present-uialertcontroller-when-not-in-a-view-controller => 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.

Figurative answered 3/11, 2016 at 12:48 Comment(2)
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.Deutoplasm
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.Prediction
D
25

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")
})
Dynamite answered 24/10, 2017 at 16:35 Comment(3)
This can be used even if sharedApplication is not accessible !Mooneye
I get the error: Unbalanced calls to begin/end appearance transitions for <UIViewController:..Newt
The alert automatically dismissed itself? Not exactly sure why it's doing thatVeterinarian
N
21

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)
Neall answered 28/12, 2015 at 4:55 Comment(4)
When I dismiss the alert, the UIWindow is nonresponsive. Something to do with the windowLevel probably. How can I make it responsive?Haler
Sounds like new window wasn't dismissed.Rye
Looks like Window does not get Removed from top, So need to remove the window once done.Thrippence
Set your alertWindow to nil when you are finished with it.Narcoma
D
14

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.

Drugstore answered 25/12, 2014 at 17:3 Comment(5)
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!Riana
@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)Boulevardier
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!Riana
Attempting to load the view of a view controller while it is deallocating is not allowed and may result in undefined behavior (<UIAlertController: 0x15cd4afe0>)Patrickpatrilateral
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 VCPleasure
S
13

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)
}
Succinic answered 10/9, 2020 at 11:6 Comment(2)
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)Amory
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.Azevedo
W
10

@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)
    }
}
Washy answered 10/1, 2018 at 16:59 Comment(2)
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.Wacke
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/uialertcontrollerKurbash
C
10

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)
    }
}
Crum answered 8/10, 2019 at 23:19 Comment(5)
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 thereDavisson
If you want to dismiss the alert manually, make sure to call dismiss on the WindowAlertPresentationController directly alert.presentingViewController?.dismiss(animated: true, completion: nil)Davisson
let alertController = UIAlertController(title: "title", message: "message", preferredStyle: .alert); alertController.presentInOwnWindow(animated: false, completion: nil) works great for me! Thanks!Millenarianism
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.Jackelynjackeroo
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 solutionDavisson
R
7

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];
Redden answered 3/6, 2015 at 11:44 Comment(0)
B
7

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

}
Baleful answered 13/12, 2017 at 16:5 Comment(2)
I tried everything above, this was literally the only answer that worked!Keeney
This works great! However the keyWindow method is deprecated from iOS 13. This fixes it.Constant
S
5

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)
}
Sather answered 11/4, 2016 at 5:52 Comment(0)
K
5

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
    }
}
Keelin answered 19/7, 2017 at 8:39 Comment(1)
@Chathuranga: I have reverted your edit. That “error handling” is completely unnecessary.Davidoff
B
5

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.

Betony answered 9/8, 2017 at 14:11 Comment(1)
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.Betony
R
5

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;
    while (rootViewController.presentedViewController != nil)
        rootViewController = rootViewController.presentedViewController;
    return rootViewController;
}

Usage:

[[AppDelegate rootViewController] presentViewController ...
Riane answered 14/10, 2019 at 18:4 Comment(4)
Does UIApplication in your code refer to your app's UIApplication? Does your code not interfere with the Share process?Eimile
@Eimile UIApplication is the system native Objective-C class. I'm not aware of any issues with the share process.Riane
I'm trying to use these codes to show an alert in my Share Extension so that the alert shows when an app shares data with my app, and the alert quickly disappears within say 1 minute. I'm posting an answer for that specifically. My cod works but the alert doesn't show, even wen a Timer object is printing to debug window every second.Eimile
I've replaced the last if with a while loop to make sure stacked presented view controllers are handled correctly as well.Riane
P
4

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

Pelagias answered 5/2, 2017 at 21:49 Comment(0)
S
4

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
        }

    }

}
Sayres answered 9/3, 2020 at 16:41 Comment(4)
UIAlerController shouldn't be subclassed according to documentation developer.apple.com/documentation/uikit/uialertcontrollerAirstrip
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 okSayres
I couldn't find any reference to UIButton shouldn't also be sub-classed. Can you please share the source?Airstrip
Thank you! Having UIScenes really messed with how to create the window, and your approach to that is nice.Bergess
R
4

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
    }

}
Regan answered 6/4, 2020 at 0:29 Comment(2)
viewDidDisappear() isn't getting called after dismissing the alert. Does anyone know why? The apparent result is that the app is frozenFiliate
@Filiate Did you figure out why viewDidDisappear() gets called, and is that a problem with a solution?Eimile
C
3

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!

Crawler answered 28/8, 2015 at 1:56 Comment(0)
U
3
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
        }
    }
}
United answered 3/8, 2017 at 5:34 Comment(0)
D
2

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

+ (void)myUtilityMethod:(id)controller {
    // do stuff
    // something bad happened, display an alert.
}
Defeatist answered 2/4, 2015 at 14:26 Comment(1)
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.Diazotize
F
2

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.

Fascist answered 20/8, 2016 at 0:43 Comment(0)
M
2

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)
        }
    }
}
Manque answered 1/12, 2017 at 10:9 Comment(0)
O
1

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];
}
Ortolan answered 6/10, 2016 at 1:32 Comment(1)
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.Mamoun
P
1

Another option:

    var topController:UIViewController = UIApplication.shared.keyWindow!.rootViewController!
    while ((topController.presentedViewController) != nil) {
        topController = topController.presentedViewController!
    }
    topController.present(alert, animated:true, completion:nil)
Phosphorism answered 28/11, 2018 at 9:24 Comment(0)
F
1

I know this was for iOS and since nearly all the links in the search engines find iOS posts I thought I would offer this up to the macOS developers.

Swift 5.5 on macOS

I added this to one of my methods in another class based off Darkngs's answer:

let alert = NSAlert()
let viewController = NSApplication.shared.keyWindow?.contentViewController
alert.messageText = "An Alert Message."
alert.addButton(withTitle: "Ok")
alert.beginSheetModal(for: (viewController?.view.window)!) {
    (returnCode: NSApplication.ModalResponse) -> Void in
}
Fabria answered 24/9, 2021 at 19:50 Comment(0)
A
0

You can try to implement a category on UIViewController with a method like - (void)presentErrorMessage;, and and inside that method you implement UIAlertController and then present it on self. Then in your client code you will have something like:

[myViewController presentErrorMessage];

In that way you'll avoid unneccessary parameters and warnings about the view not being in window hierarchy.

Ascend answered 7/4, 2015 at 11:24 Comment(3)
Except that I don't have myViewController in the code where the bad thing happens. That's in a utility method which knows nothing about the view controller that called it.Diazotize
IMHO presenting any views (thus alerts) to the user is the responsibility of ViewControllers. So if some part of the code knows nothing about viewController it shouldn't present any errors to user but rather pass them to "viewController aware" parts of the codeAscend
I agree. But the convenience of the now deprecated UIAlertView led me to break that rule in a few spots.Diazotize
V
0

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 ...];
Virtuous answered 15/9, 2015 at 14:59 Comment(0)
L
0

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;
}
Lagasse answered 20/7, 2016 at 23:50 Comment(0)
G
0

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);
    }
}
Gutbucket answered 27/12, 2016 at 20:35 Comment(0)
S
0

@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
  }
}
Sand answered 13/3, 2017 at 17:1 Comment(0)
W
0

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)
Whithersoever answered 1/12, 2017 at 12:40 Comment(0)
C
0

Swift 4+ / iOS 13:

Based on https://mcmap.net/q/100026/-how-to-present-uialertcontroller-when-not-in-a-view-controller which uses the .keyWindow method which is deprecated from iOS 13.

extension UIAlertController {
  func show(animated: Bool = true, completion: (() -> Void)? = nil) {
    if let visibleViewController = UIApplication.shared.windows.filter({$0.isKeyWindow}).first?.visibleViewController {
      visibleViewController.present(self, animated: animated, completion: completion)
    }
  }
}

(I can not edit the original answer as the edit queue is full.

Constant answered 22/11, 2022 at 13:6 Comment(0)
G
-2

Register for a notification prior to calling the class method.

Swift code:

NSNotificationCenter.defaultCenter().addObserver(self, selector: "displayAlert", name: "ErrorOccured", object: nil)

In the displayAlert instance method you could display your alert.

Grimona answered 24/10, 2014 at 19:33 Comment(1)
Thanks runios. I see that your answer works but myUtilityMethod gets called from a lot of view controllers. And the idea with the utility method is to make it easy to call. Adding an observer and handling the notification each time it's needed removes all the convenience.Diazotize
W
-2

Swift 5

I just created a new window and added alert view controller in it.

Check my class TopViewController:

https://gist.github.com/odnaks/3f3fd0d20f318c6276e76d0f9d7de5a7

I use it very simple, like UIAlertController:

 let alert = TopAlertController()
 alert.title = "title"
 alert.message = "message"
 alert.addAction(UIAlertAction(title: "Ок", style: .default, handler: { _ in }))
 alert.show()
Woodbine answered 7/10, 2021 at 9:43 Comment(1)
whyy dislike? :(Woodbine

© 2022 - 2024 — McMap. All rights reserved.