iOS 15: UIApplication.shared.keyWindow is nil, leading to a crash

Hello,

I'm investigating a crash where keyWindow is nil when I don't expect it to be nil.

I would like to understand when this can happen. Indeed,I could make a fix by guarding for nil values, but this would lead to an invalid state.

Context

  1. We want to return quickly from application(didFinishLaunchingWithOptions:), so:
  • We set a view controller as a splash screen rootViewController => we mark the window with makeKeyAndVisible().
  • We queue initializations asynchronously on the main queue.

=> Basically, while the app is initializing starting, we're displaying a splash screen view controller.

  1. When the app is done initializing, it needs to present the actual UI. A last asynchronous task on the main queue does this. We get keyWindow from UIApplication to set the new view controller with the actual UI. That's where we assume that it shouldn't be nil and force-unwrap it, but alas, in some instances it's nil.

Misc

  • This crash only happens when app built with Xcode 13.x, running on iOS 15.
  • I cannot reproduce this bug, and it has fairly little occurrence. (100s over 100000 of sessions)
  • I also attached a sample crash

Questions

  1. Given that I made the window "key and visible" in step 1, what could cause the window to stop becoming "key".

  2. What would be the correct way to get the window to set the new root view controller ?

  3. I don't really want to guard against a nil value because then it means that I wouldn't be able to set my new rootViewController and the app would be stuck on the launch screen.

Thanks a lot!

Bruno

Replies

-[UIApplication keyWindow] is deprecated and shouldn't be used for apps on iOS 15. It appears you're using it as a proxy for your app's "main window" where the app content is presented. The key window just returns the window that receives keyboard events, which may or may not be your app's main window depending on the state of the app. You shouldn't use -keyWindow for this purpose.

So how do you get your app's "main window"? That depends on what lifecycle you're using.

Application lifecycle

I'm assuming your app is still using the application lifecycle (as opposed to the scene lifecycle), since you're setting up your app's main window in application(didFinishLaunchingWithOptions:).

It is recommended that you define a window property on your AppDelegate, and when you create your app's main window in application(didFinishLaunchingWithOptions:), you store that window in the AppDelegate.window property.

Then, you can reference that window later on (from UIApplication.shared.delegate.window) whenever you want to get your app's main window.

Note that if your app adopts storyboards, your app delegate is required to have a window property: https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623056-window New Xcode project templates already adopt storyboards, and they already add the window property by default. But even if your app doesn't adopt storyboards, you can still define a window to store the window your app creates for its content.

Scene lifecycle

The scene lifecycle is recommended for all apps on iOS 13 and later (and if your app supports multiple windows, then your app must adopt the scene lifecycle).

This is very similar to the app lifecycle. You should define a window property on your SceneDelegate, and when you create your scene's main window in scene(_:willConnectTo:options:), you should store the window in that property.

And you can later access the scene's main window from your window scene's delegate: (windowScene.delegate as? SceneDelegate)?.window.

Note that since your app may have multiple window scenes, you should ensure you pick the right window scene (otherwise, you may kick off work in one window and then try to update a totally different window). You typically get the relevant window scene from a UIWindow in your app, so if you're performing work in a view controller and want to get the window scene the view controller is in, you could use self.view.window?.windowScene.

If you're kicking off an asynchronous task from somewhat-global code, you should probably store the relevant window scene somewhere so that, when the task finishes, you can use that to access its delegate's window (and make sure to pay attention to scene lifecycle methods so that, if the scene disconnects and the window is torn down during the task, you don't try to access the window later).

Similar to the application lifecycle, your scene delegate must have a window property if the scene adopts storyboards (which is done automatically in Xcode's new project template).

Hello,

Thanks a lot for this answer. I'm using the Application Lifecycle. This answers clearly how to fix this. However, I would like to clarify a point to make sure I understand properly:

The key window just returns the window that receives keyboard events,

which may or may not be your app's main window depending on the state of the app.

Could you point to states where the main window isn't the key window on iOS (I only have a single window defined by myself in my app).

Thanks!

Bruno

  • Certain kinds of system alerts and system UI that appears in the app may use their own (system-managed) windows, and those windows may become key as they appear. Your app may not have control over the presentation of that UI, so you can't assume that the key window is also the window containing your app's main content.

Add a Comment

That's very clear, thank you.

What is the best approach to this issue if making a SwiftUI app with @main entry point rather than an AppDelegate or SceneDelegate?