53

I'm trying to have an helper class that presents an UIAlertController. Since it's a helper class, I want it to work regardless of the view hierarchy, and with no information about it. I'm able to show the alert, but when it's being dismissed, the app crashed with:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException',
reason: 'Trying to dismiss UIAlertController <UIAlertController: 0x135d70d80>
 with unknown presenter.'

I'm creating the popup with:

guard let window = UIApplication.shared.keyWindow else { return }
let view = UIView()
view.isUserInteractionEnabled = true
window.insertSubview(view, at: 0)
window.bringSubview(toFront: view)
// add full screen constraints to view ...

let controller = UIAlertController(
  title: "confirm deletion?",
  message: ":)",
  preferredStyle: .alert
)

let deleteAction = UIAlertAction(
  title: "yes",
  style: .destructive,
  handler: { _ in
    DispatchQueue.main.async {
      view.removeFromSuperview()
      completion()
    }
  }
)
controller.addAction(deleteAction)

view.insertSubview(controller.view, at: 0)
view.bringSubview(toFront: controller.view)
// add centering constraints to controller.view ...

When I tap yes, the app will crash and the handler is not being hit before the crash. I can't present the UIAlertController because this would be dependent of the current view hierarchy, while I want the popup to be independant

EDIT: Swift solution Thanks @Vlad for the idea. It seems that operating in a separate window is much more simple. So here is a working Swift solution:

class Popup {
  private var alertWindow: UIWindow
  static var shared = Popup()

  init() {
    alertWindow = UIWindow(frame: UIScreen.main.bounds)
    alertWindow.rootViewController = UIViewController()
    alertWindow.windowLevel = UIWindowLevelAlert + 1
    alertWindow.makeKeyAndVisible()
    alertWindow.isHidden = true
  }

  private func show(completion: @escaping ((Bool) -> Void)) {
    let controller = UIAlertController(
      title: "Want to do it?",
      message: "message",
      preferredStyle: .alert
    )

    let yesAction = UIAlertAction(
      title: "Yes",
      style: .default,
      handler: { _ in
        DispatchQueue.main.async {
          self.alertWindow.isHidden = true
          completion(true)
        }
    })

    let noAction = UIAlertAction(
      title: "Not now",
      style: .destructive,
      handler: { _ in
        DispatchQueue.main.async {
          self.alertWindow.isHidden = true
          completion(false)
        }
    })

    controller.addAction(noAction)
    controller.addAction(yesAction)
    self.alertWindow.isHidden = false
    alertWindow.rootViewController?.present(controller, animated: false)
  }
}
Kai
  • 38,985
  • 14
  • 88
  • 103
Guig
  • 9,891
  • 7
  • 64
  • 126

12 Answers12

111

Update Dec 16, 2019:

Just present the view controller/alert from the current top-most view controller. That will work :)

if #available(iOS 13.0, *) {
     if var topController = UIApplication.shared.keyWindow?.rootViewController  {
           while let presentedViewController = topController.presentedViewController {
                 topController = presentedViewController
                }
     topController.present(self, animated: true, completion: nil)
}

Update July 23, 2019:

IMPORTANT

Apparently the method below this technique stopped working in iOS 13.0 :(

I'll update once I find the time to investigate...

Old technique:

Here's a Swift (5) extension for it:

public extension UIAlertController {
    func show() {
        let win = UIWindow(frame: UIScreen.main.bounds)
        let vc = UIViewController()
        vc.view.backgroundColor = .clear
        win.rootViewController = vc
        win.windowLevel = UIWindow.Level.alert + 1  // Swift 3-4: UIWindowLevelAlert + 1
        win.makeKeyAndVisible()    
        vc.present(self, animated: true, completion: nil)
    }
}

Just setup your UIAlertController, and then call:

alert.show()

No more bound by the View Controllers hierarchy!

SwiftiSwift
  • 7,528
  • 9
  • 56
  • 96
jazzgil
  • 2,250
  • 2
  • 19
  • 20
  • 2
    really, this is perfect. Good one. – Fattie Jun 21 '17 at 11:29
  • 17
    So how would you then hide it? – Erik Grosskurth Jun 23 '17 at 18:15
  • @jazzgil https://stackoverflow.com/questions/44728819/ios-10-swift-3-cannot-dismiss-uialertcontroller-from-singleton-instance/44729045?noredirect=1#comment76441312_44729045 – Erik Grosskurth Jun 23 '17 at 20:15
  • This is the proper way to do it. The accepted answer (by Vlad) is not enough and doesn't do the job. – Vahid Amiri Mar 25 '18 at 16:07
  • This should be the first answer. – Mihail Salari May 05 '18 at 14:31
  • This is the best answer so far. – khunshan Sep 10 '18 at 22:18
  • It's inadvisable to use UIScreen.main.bounds as this will be incorrect if the user is running the code in a split-screen mode on the iPad. It may be better to use the rootViewController. – Josh Paradroid Oct 19 '18 at 15:39
  • 1
    Alerts on a new window looks good if your app supports one orientation only. But if you want to deal with UI rotation, especially if different View Controllers have their own supported orientation configuration, then there will be a lot more works to do. In fact, I haven't found a solution to handle orientation well. – Henry H Miao Oct 26 '18 at 06:20
  • 2
    In Xcode 11 and iOS 13 beta, this answer (which I have been using a long time), causes the alert to show, then disappear in approximately 0.5 seconds. Anyone have enough time in the new beta to have any idea why? – Matthew Bradshaw Jun 11 '19 at 17:48
  • This literally caused me hours of wondering why my ui is frozen. The `UIWindow` object kept blocking the whole screen (even after `dismiss`!). I had to `UIWindow.DangerousAutorelease()` it in Xamarin.iOS. – iSpain17 Jul 17 '19 at 17:17
  • What is alert ? – Sam Oct 17 '19 at 20:28
  • 1
    @MatthewBradshaw I experienced the same disappearing issue. The issue is the window instance is local to the function. Once the function runs the window is deallocated. – Lance Samaria Dec 25 '19 at 23:51
  • Just move 'let win = UIWindow(frame: UIScreen.main.bounds)' and 'let vc = UIViewController()' out of the function and have them as strong reference. – Arijan May 28 '20 at 06:40
17

I will rather present it on UIApplication.shared.keyWindow.rootViewController, instead of using your logic. So you can do next:

UIApplication.shared.keyWindow.rootViewController.presentController(yourAlert, animated: true, completion: nil)

EDITED:

I have an old ObjC category, where I've used the next method show, which I used, if no controller was provided to present from:

- (void)show
{
    self.alertWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
    self.alertWindow.rootViewController = [UIViewController new];
    self.alertWindow.windowLevel = UIWindowLevelAlert + 1;
    [self.alertWindow makeKeyAndVisible];
    [self.alertWindow.rootViewController presentViewController: self animated: YES completion: nil];
}

added entire category, if somebody need it

#import "UIAlertController+ShortMessage.h"
#import <objc/runtime.h>

@interface UIAlertController ()
@property (nonatomic, strong) UIWindow* alertWindow;
@end

@implementation UIAlertController (ShortMessage)

- (void)setAlertWindow: (UIWindow*)alertWindow
{
    objc_setAssociatedObject(self, @selector(alertWindow), alertWindow, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIWindow*)alertWindow
{
    return objc_getAssociatedObject(self, @selector(alertWindow));
}

+ (UIAlertController*)showShortMessage: (NSString*)message fromController: (UIViewController*)controller
{
    return [self showAlertWithTitle: nil shortMessage: message fromController: controller];
}

+ (UIAlertController*)showAlertWithTitle: (NSString*)title shortMessage: (NSString*)message fromController: (UIViewController*)controller
{
    return [self showAlertWithTitle: title shortMessage: message actions: @[[UIAlertAction actionWithTitle: @"Ok" style: UIAlertActionStyleDefault handler: nil]] fromController: controller];
}

+ (UIAlertController*)showAlertWithTitle: (NSString*)title shortMessage: (NSString*)message actions: (NSArray<UIAlertAction*>*)actions fromController: (UIViewController*)controller
{
    UIAlertController* alert = [UIAlertController alertControllerWithTitle: title
                                                    message: message
                                             preferredStyle: UIAlertControllerStyleAlert];

    for (UIAlertAction* action in actions)
    {
        [alert addAction: action];
    }

    if (controller)
    {
        [controller presentViewController: alert animated: YES completion: nil];
    }
    else
    {
        [alert show];
    }

    return alert;
}

+ (UIAlertController*)showAlertWithMessage: (NSString*)message actions: (NSArray<UIAlertAction*>*)actions fromController: (UIViewController*)controller
{
    return [self showAlertWithTitle: @"" shortMessage: message actions: actions fromController: controller];
}

- (void)show
{
    self.alertWindow = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
    self.alertWindow.rootViewController = [UIViewController new];
    self.alertWindow.windowLevel = UIWindowLevelAlert + 1;
    [self.alertWindow makeKeyAndVisible];
    [self.alertWindow.rootViewController presentViewController: self animated: YES completion: nil];
}

@end
Vladyslav Zavalykhatko
  • 15,202
  • 8
  • 65
  • 100
11

Old approach with adding show() method and local instance of UIWindow no longer works on iOS 13 (window is dismissed right away).

Here is UIAlertController Swift extension which should work on iOS 13:

import UIKit

private var associationKey: UInt8 = 0

extension UIAlertController {

    private var alertWindow: UIWindow! {
        get {
            return objc_getAssociatedObject(self, &associationKey) as? UIWindow
        }

        set(newValue) {
            objc_setAssociatedObject(self, &associationKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
        }
    }

    func show() {
        self.alertWindow = UIWindow.init(frame: UIScreen.main.bounds)
        self.alertWindow.backgroundColor = .red

        let viewController = UIViewController()
        viewController.view.backgroundColor = .green
        self.alertWindow.rootViewController = viewController

        let topWindow = UIApplication.shared.windows.last
        if let topWindow = topWindow {
            self.alertWindow.windowLevel = topWindow.windowLevel + 1
        }

        self.alertWindow.makeKeyAndVisible()
        self.alertWindow.rootViewController?.present(self, animated: true, completion: nil)
    }

    override open func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        self.alertWindow.isHidden = true
        self.alertWindow = nil
    }
}

Such UIAlertController then can be created and shown like this:

let alertController = UIAlertController(title: "Title", message: "Message", preferredStyle: .alert)
let alertAction = UIAlertAction(title: "Title", style: .default) { (action) in
    print("Action")
}

alertController.addAction(alertAction)
alertController.show()
Maxim Makhun
  • 2,197
  • 1
  • 22
  • 26
  • 1
    this solution is working for me in iOS13. Great Job Thank You :) – Yogesh Patel Sep 23 '19 at 12:01
  • 3
    I want to warn you of this solution. As the extension overrides viewDidDisappear, this leads to crashes, if you didn't present using the show() method! – Simon C. Oct 06 '19 at 11:26
  • 3
    This can be easily fixed by converting alertWindow to an optional, or adding assert to viewDidDisappear (so that developer understands that show() method has to be called beforehand). And my recommendation: make sure you understand how API works before using it :) – Maxim Makhun Oct 06 '19 at 13:14
9

in Swift 4.1 and Xcode 9.4.1

I'm calling alert function from my shared class

//This is my shared class
import UIKit

class SharedClass: NSObject {

static let sharedInstance = SharedClass()
    //This is alert function
    func alertWindow(title: String, message: String) {
        let alertWindow = UIWindow(frame: UIScreen.main.bounds)
        alertWindow.rootViewController = UIViewController()
        alertWindow.windowLevel = UIWindowLevelAlert + 1

        let alert2 = UIAlertController(title: title, message: message, preferredStyle: .alert)
        let defaultAction2 = UIAlertAction(title: "OK", style: .default, handler: { action in
        })
        alert2.addAction(defaultAction2)

        alertWindow.makeKeyAndVisible()
        alertWindow.rootViewController?.present(alert2, animated: true, completion: nil)
    }
    private override init() {
    }
}

I'm calling this alert function in my required view controller like this.

//I'm calling this function into my second view controller
SharedClass.sharedInstance.alertWindow(title:"Title message here", message:"Description message here")
Naresh
  • 16,698
  • 6
  • 112
  • 113
4

Swift 3 example

let alertWindow = UIWindow(frame: UIScreen.main.bounds)
alertWindow.rootViewController = UIViewController()
alertWindow.windowLevel = UIWindowLevelAlert + 1

let alert = UIAlertController(title: "AlertController Tutorial", message: "Submit something", preferredStyle: .alert)

alertWindow.makeKeyAndVisible()
alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
Lijith Vipin
  • 1,870
  • 21
  • 29
4

The often cited solution using a newly created UIWindow as a UIAlertController extension stopped working in iOS 13 Betas (looks like there is no strong reference held by iOS to the UIWindow anymore, so the alert disappears immediately).

The below solution is slightly more complex, but works in iOS 13.0 and older versions of iOS:

class GBViewController: UIViewController {
    var didDismiss: (() -> Void)?
    override func dismiss(animated flag: Bool, completion: (() -> Void)?)
    {
        super.dismiss(animated: flag, completion:completion)
        didDismiss?()
    }
    override var prefersStatusBarHidden: Bool {
        return true
    }
}

class GlobalPresenter {
    var globalWindow: UIWindow?
    static let shared = GlobalPresenter()

    private init() {
    }

    func present(controller: UIViewController) {
        globalWindow = UIWindow(frame: UIScreen.main.bounds)
        let root = GBViewController()
        root.didDismiss = {
            self.globalWindow?.resignKey()
            self.globalWindow = nil
        }
        globalWindow!.rootViewController = root
        globalWindow!.windowLevel = UIWindow.Level.alert + 1
        globalWindow!.makeKeyAndVisible()
        globalWindow!.rootViewController?.present(controller, animated: true, completion: nil)
    }
}

Usage

    let alert = UIAlertController(title: "Alert Test", message: "Alert!", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
    GlobalPresenter.shared.present(controller: alert)
AndreasB
  • 41
  • 1
4

If you're trying to present a UIActivityController in a modally presented UIViewController, you need to present from the presentedViewController. Otherwise, nothing gets presented. I use this method in iOS 13 to return the active UIViewController:

func activeVC() -> UIViewController? {
    // Use connectedScenes to find the .foregroundActive rootViewController
    var rootVC: UIViewController?
    for scene in UIApplication.shared.connectedScenes {
        if scene.activationState == .foregroundActive {
            rootVC = (scene.delegate as? UIWindowSceneDelegate)?.window!!.rootViewController
            break
        }
    }
    // Then, find the topmost presentedVC from it.
    var presentedVC = rootVC
    while presentedVC?.presentedViewController != nil {
        presentedVC = presentedVC?.presentedViewController
    }
    return presentedVC
}

So, for example:

activeVC()?.present(activityController, animated: true)
Steve Harris
  • 206
  • 2
  • 6
4

This works for me for iOS 13.1, Xcode 11.5 by combining answers of Ruslan and Steve.

func activeVC() -> UIViewController? {

let appDelegate = UIApplication.shared.delegate as! AppDelegate

var topController: UIViewController = appDelegate.window!.rootViewController!
while (topController.presentedViewController != nil) {
    topController = topController.presentedViewController!
}

return topController 
}

usages:

activeVC()?.present(alert, animated: true)
Muzammil
  • 1,529
  • 1
  • 15
  • 24
3

My own iOS 13 workaround.

Edit notice : I edited my previous answer because, as others solutions around, it used a redefinition of viewWillDisappear: which is incorrect in a class extension and effectively stoped working with 13.4.

This solution, based on UIWindow paradigm, defines a category (extension) on UIAlertController. In that category file we also define a simple subclass of UIViewController used to present theUIAlertController.

@interface AlertViewControllerPresenter : UIViewController
@property UIWindow *win;
@end

@implementation AlertViewControllerPresenter
- (void) dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    [_win resignKeyWindow]; //optional nilling the window works
    _win.hidden = YES; //optional nilling the window works
    _win = nil;
    [super dismissViewControllerAnimated:flag completion:completion];
}
@end

The presenter retains the window. When the presented alert is dismissed the window is released.

Then define a show method in the category (extension) :

- (void)show {
    AlertViewControllerPresenter *vc = [[AlertViewControllerPresenter alloc] init];
    vc.view.backgroundColor = UIColor.clearColor;
    UIWindow *win = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
    vc.win = win;
    win.rootViewController = vc;
    win.windowLevel = UIWindowLevelAlert;
    [win makeKeyAndVisible];
    [vc presentViewController:self animated:YES completion:nil];
}

I do realise that the OP tagged Swift and this is ObjC, but this is so straightforward to adapt…

Max_B
  • 887
  • 7
  • 16
3

working solution for TVOS 13 and iOS 13

static func showOverAnyVC(title: String, message: String) {
    let alert = UIAlertController(title: title, message: message, preferredStyle: UIAlertController.Style.alert)
    alert.addAction((UIAlertAction(title: "OK", style: .default, handler: {(action) -> Void in
    })))
    let appDelegate = UIApplication.shared.delegate as! AppDelegate

    var topController: UIViewController = appDelegate.window!.rootViewController!
    while (topController.presentedViewController != nil) {
        topController = topController.presentedViewController!
    }

    topController.present(alert, animated: true, completion: nil)
}
Ruslan Sabirov
  • 440
  • 1
  • 4
  • 19
1
 func windowErrorAlert(message:String){
    let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
    let window = UIWindow(frame: UIScreen.main.bounds)
    window.rootViewController = UIViewController()
    let okAction = UIAlertAction(title: "Ok", style: .default) { (action) -> Void in
        alert.dismiss(animated: true, completion: nil)
        window.resignKey()
        window.isHidden = true
        window.removeFromSuperview()
        window.windowLevel = UIWindowLevelAlert - 1
        window.setNeedsLayout()
    }

    alert.addAction(okAction)
    window.windowLevel = UIWindowLevelAlert + 1
    window.makeKeyAndVisible()

    window.rootViewController?.present(alert, animated: true, completion: nil)
}

Create a UIAlertController on top of all view and also dismiss and give focus back to your rootViewController.

anuraagdjain
  • 86
  • 2
  • 7
1

Below code works for iOS 13 as well as on older versions :

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let myVC = storyboard.instantiateViewController(withIdentifier: "MyVC") as! MyViewController
            
myVC.view.backgroundColor = .clear
myVC.modalPresentationStyle = .overCurrentContext
            
self.present(popup, animated: true, completion: nil)
Krishna Meena
  • 5,693
  • 5
  • 32
  • 44