An Anti-Pattern to get UIWindowScene

matsuji

matsuji

Posted on November 18, 2024

An Anti-Pattern to get UIWindowScene

Starting with iOS 13, UIWindowScene was introduced, and it is often required in cases such as showing a review request for the App Store.

When it comes to obtaining UIWindowScene, incorrect implementations are often shared on platforms like Stack Overflow.
In this article, I will share the incorrect implementations, their issues, and the correct one.

The Common Incorrect Pattern

This is an incorrect example that I often encounter.

let windowScene = UIApplication.shared
    .connectedScenes
    .compactMap { $0 as? UIWindowScene }
    .first
Enter fullscreen mode Exit fullscreen mode

This code extracts UIScene from UIApplication, casts it to UIWindowScene, and uses the first one.

If you are not familiar with Scene, the code seems to work well, and you may adopt this pattern without understanding it well.

What is An Issue?

The most obvious issue is that the code does not account for multiple windows in iPadOS and visionOS.
The type of connectedScenes is Set<UIScene>, which contains UIWindowScene objects corresponding to each window.
It means that if a user makes multiple windows, window A and window B, connectedScenes has window A and window B.

// Image
connectedScenes = [
    (UIWindowScene of window A),
    (UIWindowScene of window B),
]
Enter fullscreen mode Exit fullscreen mode

In this case, if you adopt the incorrect implementation shared above, the implementation may return UIWindowScene of window A even if you need UIWindowScene of window B.

You may need a UIWindowScene that the user is touching, but there are no APIs filling such requirements.
I sometimes found the following implementations which checks activationState, but it is also incorrect because multiple UIWindowScenes can become foregroundActive at the same time when multiple windows are foreground in iPadOS and visionOS.

let windowScene = UIApplication.shared
    .connectedScenes
    .compactMap { $0 as? UIWindowScene }
    .filter { $0.activationState == .foregroundActive } // check activationState
    .first
Enter fullscreen mode Exit fullscreen mode

Is the above code fine if we give up multiple windows in iPadOS and visionOS?
The answer is NO.
Multi windows can be possible even though in iOS.
When an iPhone is connected to an external display, the UI in the external display is on another UIWindowScene which is made newly.

If your product doesn't support iPadOS, visionOS and an external display and you really need the above code, it seems fine to use it for now.
However, note the above code is one of hack ways and it can work well only on the current iOS.
The code may not work well in the future iOS and you should evaluate whether the risk can be accepted or not.

Correct Implementations

UIKit

The easiest way is to get a UIWindowScene from a UIView.

let windowScene = view.window?.windowScene
Enter fullscreen mode Exit fullscreen mode

That's all!
Note that window is nil until the UIView has appeared.
On viewWillAppear, the window is nil and the value is set from viewIsAppearing.

SwiftUI

If you use SwiftUI, you can extract UIWindowScene from EnvironmentObject.

  1. Define AppDelegate and SceneDelegate
  2. Connect AppDelegate and your App.
  3. Make SceneDelegate conform to ObservableObject
  4. Capture UIWindow in SceneDelegate.
  5. Get SceneDelegate by EnvironmentObject and get UIWindowScene from the SceneDelegate
@main
struct FlashcardsApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        ....
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
        configuration.delegateClass = SceneDelegate.self
        return configuration
    }
}

class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { // Make SceneDelegate conform ObservableObject
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        self.window = (scene as? UIWindowScene)?.keyWindow
    }
}

struct YourView: View {
    // SceneDelegate is automatically set if it conforms to `ObservableObject`
    @EnvironmentObject var sceneDelegate: SceneDelegate
    var windowScene: UIWindowScene? {
        sceneDelegate.window?.windowScene
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Wrap Up

Please avoid incorrect implementations using connectedScenes which are shared in many places, because they are a hack.
The simplest and correct way is to get UIWindowScene from a UIView.

💖 💪 🙅 🚩
matsuji
matsuji

Posted on November 18, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related