Present UIAlertController on top of everything regardless of the view hierarchy

IosSwiftCocoa TouchCrashPopup

Ios Problem Overview


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

Ios Solutions


Solution 1 - Ios

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!

Solution 2 - Ios

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

Solution 3 - Ios

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

Solution 4 - Ios

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

Solution 5 - Ios

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)

Solution 6 - Ios

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)

Solution 7 - Ios

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)

Solution 8 - Ios

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…

Solution 9 - Ios

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

Solution 10 - Ios

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)

Solution 11 - Ios

 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.

Solution 12 - Ios

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)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGuigView Question on Stackoverflow
Solution 1 - IosjazzgilView Answer on Stackoverflow
Solution 2 - IosVladyslav ZavalykhatkoView Answer on Stackoverflow
Solution 3 - IosMaxim MakhunView Answer on Stackoverflow
Solution 4 - IosNareshView Answer on Stackoverflow
Solution 5 - IosLijith VipinView Answer on Stackoverflow
Solution 6 - IosAndreasBView Answer on Stackoverflow
Solution 7 - IosMuzammilView Answer on Stackoverflow
Solution 8 - IosMax_BView Answer on Stackoverflow
Solution 9 - IosRuslan SabirovView Answer on Stackoverflow
Solution 10 - IosSteve HarrisView Answer on Stackoverflow
Solution 11 - IosanuraagdjainView Answer on Stackoverflow
Solution 12 - IosKrishna MeenaView Answer on Stackoverflow