UIAlertController disappearing since iOS 13

Good afternoon everyone, I have a problem with showing Alerts in iOS 13, my code is:


On AppDelegate:


func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
        // Override point for customization after application launch.
        debugPrint("didFinishLaunchingWithOptions")
        let alert = UIAlertController(title: "My APP", message:"didFinishLaunchingWithOptions", preferredStyle: UIAlertController.Style.alert)
        alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { (action: UIAlertAction!) in
            debugPrint("OK")
        }))
        self.showAlertGlobally(alert)
        return true
}


On Custom View Controller:


override func viewDidLoad()
{
    super.viewDidLoad()
    // Do any additional setup after loading the view.
    let alert = UIAlertController(title: "My App", message:"viewDidLoad", preferredStyle: UIAlertController.Style.alert)
    alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: { (action: UIAlertAction!) in
        debugPrint("OK")
    }))
    self.showAlertGlobally(alert)
}


And my custom method:


func showAlertGlobally(_ alert: UIAlertController)
    {
        let alertWindow = UIWindow(frame: UIScreen.main.bounds)
        alertWindow.windowLevel = UIWindow.Level.alert
        alertWindow.rootViewController = UIViewController()
        alertWindow.makeKeyAndVisible()
        alertWindow.rootViewController?.present(alert, animated: true, completion: nil)
    }


it was working on iOS 12, the method was showing the Alert from the AppDelegate and any ViewController, but every UIAlertController disappear automatically before user responds since iOS 13


I tried this solutions:https://stackoverflow.com/questions/58131996/every-uialertcontroller-disappear-automatically-before-user-responds-since-ios and https://stackoverflow.com/questions/58148334/uialertcontroller-disappearing-since-ios-13, but still not working, if show the alert all view lose and disable the touch property in any item

Replies

I'm having the same issue, also only occurs on iOS 13.
iOS 11 and 12 both function as desired with the UIAlertController's ActionSheet style, but running the same code on iOS 13 results in a popover which disappears as soon as the popover's presentation animation is complete.
A user cannot interact with the UIAlertController before it disappears.
I have yet to find a workaround, has anyone else had better luck?

Why do you need to create a new window to show the alert. You are in a view controller. Simply present the alert controller just like you would present any view controller:


present(alert, animated: true, completion: nil)


The problem with your showAlertGlobally method is that you must create the window for a specific UIWindowScene. And since an iOS 13 app on an iPad may have multiple scenes, it doesn't make sense to have a global method for showing an alert. How would it know which scene to show it from? Just present the alert normally and all of those issues go away.

>Why do you need to create a new window to show the alert. You are in a view controller. Simply present the alert controller just like you would present any view controller:


In the simple case, yes. But if you do anything asynchronously, that could result in an error (run an operation that talks to a server and get an error) and your view controller goes off screen in between, you may end up getting one of these beauties in your console if you presented the alert straight from your calling view controller:


>Presenting a view controller on detached view controllers is discouraged.


Or


>Attempted to present a view controller (UIAlertController) on view controller which is already presenting view controller (Someother view controller)


Also, if you have some like really important global error. Like if your app requires log in and the user is logged out of his account and needs to authenticate you may want to present an alert globally. It would be very tedious to write code to dig through the view controller hierarchy to find the view controller you'd need to present on. And if you DO, you may unintentionally interfere with something that view controller is doing (like it may be just about to present ANOTHER view controller on its own) and you just hijacked it, so then you'd get:


>Attempted to present a view controller (SomeVC) on view controller which is already presenting view controller (UIAlertCOntroller)

>but still not working, if show the alert all view lose and disable the touch property in any item


Are you cleaning up the window hierarchy when the alert controller goes away (via -removeFromSuperview)? Sounds like you might be leaving orphaned windows in the window hierarchy, which may be why you aren't getting touches.

Some fair points but it doesn't change the fact that using a global alert fails under iOS 13. The window needs to be tied to a specific window scene under iOS 13.

Haven't had time to explore multiple windows too much yet (looked for a sample project, but only found a pretty disappointing sample that just had two duplicate windows open, mimicking multiple app instances).


Presumably, there is a way to determine the focused window? I'd choose the focused one I guess? I know on iOS though that isn't always an obvious thing to tell, visually. I could see an app showing a login prompt on all "windows" (app instances, from the users perspective)...and I guess the first one the user interacted with, could send a message that would dismiss them all in every scene. Which doesn't sound great...


macOS is a much more mature platform for these kind of things. Hope they don't deprecate AppKit and replace it with this stuff. My God. [NSApp presentError:globalError];


LoginWindowController *loginWindowController = [[LoginWindowController alloc]init];
[loginWindowController showWindow:nil];

This code works for me (Objective C). It covers the problem of presenting an alert when a previous alert is in the process of being presented and it works from non-viewcontroller classes.




NSDate *lastAlertDate; 

-(void)showAlert:(UIAlertController *)alert{ 
    if(!lastAlertDate)lastAlertDate=[NSDate dateWithTimeIntervalSinceNow:-1.0]; 
    if([lastAlertDate timeIntervalSinceNow]>-.5){ 
        [self performSelector:@selector(showAlert:) withObject:alert afterDelay:0.5]; 
        return; 
    } 
    lastAlertDate=[NSDate dateWithTimeIntervalSinceNow:0.0]; 
    UIViewController* parentController =[[UIApplication sharedApplication]keyWindow].rootViewController; 
    while( parentController.presentedViewController && 
          parentController != parentController.presentedViewController ){ 
        parentController = parentController.presentedViewController; 
    } 
    [parentController presentViewController:alert animated:YES completion:nil]; 
}
  • I again had this problem and searched and found my solution from 3 years ago - thank you/me.

Add a Comment

Hi!


I've had the same problem. For some reason on iOS 13 it is necessary to keep a reference to the window on which the alert is present.

This should work:


import UIKit

extension UIAlertController {
   
    private static var globalPresentationWindow: UIWindow?
   
    func presentGlobally(animated: Bool, completion: (() -> Void)?) {
        UIAlertController.globalPresentationWindow = UIWindow(frame: UIScreen.main.bounds)
        UIAlertController.globalPresentationWindow?.rootViewController = UIViewController()
        UIAlertController.globalPresentationWindow?.windowLevel = UIWindowLevelAlert + 1
        UIAlertController.globalPresentationWindow?.backgroundColor = .clear
        UIAlertController.globalPresentationWindow?.makeKeyAndVisible()
        UIAlertController.globalPresentationWindow?.rootViewController?.present(self, animated: animated, completion: completion)
    }
   
    open override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        UIAlertController.globalPresentationWindow?.isHidden = true
        UIAlertController.globalPresentationWindow = nil
    }
   
}


Hope this helps!

This wouldn't take into account a view controller that could potentially present another view controller after an operation is completed in the background.


[modelObject loadDataWithCompletionHander:^(NSArray*someData, NSError *errorOrNil)
{
    if (someData.count != nil)
    {
       DataViewController *dataVC = //....yadayada
       [self presentViewController:dataVC animated:YES completion:nil];
     }
     else
    {
           //show error.
    } 

}];


If your alert got presented before the completion block is called it would block the view controller from presenting ( and iOS will spit out the warning log I mentioned in a previous post). You could argue that a view controller ought to check if it's on screen before presenting a view controller always and defer the action if/when it comes back, though that could really end up being very tedious.

I do something similiar to this. Though I'd either subclass UIAlertController or swizzle viewDidDisappear. You shouldn't override a method of a primary class in an ObjC category (or Swift extension) I don't think.

Your concern is a very real one that remains a real concern and is not addressed in your code. However, it is specifically addressed by changing your command:

[self presentViewController:dataVC animated:YES completion:nil];

to:

[showAlert dataVC];

There are still view controllers you don't have control over (system provided view controllers or view controllers provided by third party libraries). I'm much more comfortable presenting it in a separate window instead of injecting alerts inside a stack of presented view controllers so I don't break things unintentionally. You'd basically have to route all presentation calls through a method similar to yours.

Unfortunately, it's not working for me using iOS 13.4.

The completion closure works fine, but it does not show any alert, just a black screen.


Any other ideas?

I tested on iOS 13.4.1 (on device and simulator) and it still work correctly for me.

By the way, keep in mind that what Macho Man ***** Savage said is correct. The right way to do it is to subclass UIAlertController because we shouldn't override inside extensions.


You can try this code:


class GlobalAlertController: UIAlertController {
    
    var globalPresentationWindow: UIWindow?
    
    func presentGlobally(animated: Bool, completion: (() -> Void)?) {
        globalPresentationWindow = UIWindow(frame: UIScreen.main.bounds)
        globalPresentationWindow?.rootViewController = LightStatusBarController()
        globalPresentationWindow?.windowLevel = UIWindow.Level.alert + 1
        globalPresentationWindow?.backgroundColor = .clear
        globalPresentationWindow?.makeKeyAndVisible()
        globalPresentationWindow?.rootViewController?.present(self, animated: animated, completion: completion)
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        globalPresentationWindow?.isHidden = true
        globalPresentationWindow = nil
    }

}
I am facing similar issue in case of a system provided view controllers. The password save alert controller is shown when the user login. But if the app is moved to background and comes back to foreground the alert controller disappears. Any idea how to resolve this?