Present UIAlertController on top of everything regardless of the view hierarchy
IosSwiftCocoa TouchCrashPopupIos 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)