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
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),
]
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 UIWindowScene
s 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
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
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
.
- Define
AppDelegate
andSceneDelegate
- Connect
AppDelegate
and your App. - Make
SceneDelegate
conform toObservableObject
- Capture
UIWindow
inSceneDelegate
. - Get
SceneDelegate
by EnvironmentObject and getUIWindowScene
from theSceneDelegate
@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
}
...
}
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
.
Top comments (0)