SwiftUI App Life Cycle で View が属している keyWindow (UIWindow) を取得する
課題
SwiftUI でアプリを書いる場合であっても、何らかの理由で View が属している UIWindow を取得したくなることがたまにあるかと思います。
Scene の概念が増えたことによって、従来の UIApplication.shared.keyWindow で取得する方法については iOS 13 で deprecated になっており、代わりにUIApplication.shared.windows.first(where: \.isKeyWindow) のように取得するパターンがあるかと思いますが、こちらも iOS 15 で deprecated となっています。また、自身が所属している Scene の keyWindow を正しく判断することができません。
では、今後はどの様に keyWindow を取得すれば良いのでしょうか。
この記事では SwiftUI App Life Cycle を利用した場合の解決案を記載しますが、この記事の結論としては、結局のところ UISceneDelegate を利用する方法となるので、従来型の UIKit App Delegate の Life Cycle と同じような実装を行うことになりました。
実装例
というわけで、早速実装例です。今回の実装の全体像は以下のようになります。
ポイントとしては、
-
UIApplicationDelegateAdapterを設定して自作のAppDelegateを利用する -
AppDelegateで自作のSceneDelegateを利用するように設定する -
SceneDelegate内でUIWindowSceneからkeyWindow(UIWindow) を取り出す -
SceneDelegateをObservableObjectに適合させる
というところになります。
@main
struct GetWindowDemoApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
final 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
}
}
final class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window = (scene as? UIWindowScene)?.keyWindow
}
}
UIApplicationDelegateAdapter を設定して自作の AppDelegate を利用する
こちらは主に自作の SceneDelegate を利用するために必要です。特に難しいことはないですが、公式のドキュメントを参考に実装しています。
import SwiftUI
@main
struct GetWindowDemoApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
final class AppDelegate: NSObject, UIApplicationDelegate {}
AppDelegate で自作の SceneDelegate を利用するように設定する
こちらも公式のドキュメント(Scene Delegates のセクション)を参考に、自作の SceneDelegate を利用するように設定します。
final 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
}
}
final class SceneDelegate: NSObject, UIWindowSceneDelegate {}
SceneDelegate 内で UIWindowScene から keyWindow (UIWindow) を取り出す
UIWindowSceneDelegate に準拠した SceneDelegate クラスを作成して scene(_:willConnectTo:options:) を実装します。そしてそのメソッドの第一引数から UIWindowScene を取り出し、keyWindow を SceneDelegate の window プロパティに保持しておきます。
(ちなみにこの window プロパティは、UIWindowSceneDelegate の実装任意なプロパティになっています)
UIKit App Delegate のパターンでは、このメソッドで keyWindow を作ったり、UIWindow に rootViewController を設定したりすると思うのですが、SwiftUI App Life Cycle の場合、既に keyWindow や rootViewController が設定済みの UIWindowScene を取得することができました。
final class SceneDelegate: NSObject, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window = (scene as? UIWindowScene)?.keyWindow
// iOS 14 以前をサポートする場合は以下のようにすると良さそう
// window = (scene as? UIWindowScene)?.windows.first(where: \.isKeyWindow)
}
}
SceneDelegate を ObservableObject に適合させる
ドキュメントに記載の通り、AppDelegate や SceneDelegate をObservableObject に適合させておくと、自動的に SwiftUI の Environment に設定してくれるため、@EnvironmentObject var sceneDelegate: SceneDelegate などとしてどの View からでも SceneDelegate を取り出すことができるようになります。これによって、どの View からでも自身が所属する Scene の keyWindow に SceneDelegate を通じてアクセスできるようになります。
final class SceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
// 省略
}
As with the app delegate, if you make your scene delegate an observable object, SwiftUI automatically puts it in the Environment, from where you can access it with the EnvironmentObject property wrapper, and create bindings to its published properties.
実装の基礎部分としては以上になります。
利用してみる
以下のようなコードを書いてみて、iPad で Window を二つ開き、それぞれの正しい window を取得できているかを確認しました
struct ContentView: View {
@State var isPresented = false
var body: some View {
VStack {
Button("sheet") {
isPresented = true
}
}
.sheet(isPresented: $isPresented) {
SheetView()
}
}
}
struct SheetView: View {
// こんな感じでとれる
@EnvironmentObject var sceneDelegate: SceneDelegate
var body: some View {
Button("dismiss") {
sceneDelegate.window?.rootViewController?.dismiss(animated: true)
// 以下のやり方は意図通り動くものの deprecated
//UIApplication.shared.keyWindow?.rootViewController?.dismiss(animated: true)
// 以下のやり方だと、自身が所属していない Scene の keyWindow を触ってしまう場合がある
// また、windows が deprecated になっている
// UIApplication.shared.windows.first(where: \.isKeyWindow)?.rootViewController?.dismiss(animated: true)
// 以下のやり方だと、自身が所属していない Scene の keyWindow を触ってしまう場合がある
// UIApplication.shared.connectedScenes
// .compactMap { $0 as? UIWindowScene }
// .flatMap(\.windows)
// .first(where: \.isKeyWindow)?.rootViewController?.dismiss(animated: true)
}
}
}
動作例
| 意図通りの例 | 意図しない例 |
|---|---|
![]() |
![]() |
よく見る解答例
他のやり方として、UIApplication.shared.connectedScene から UIWindowScene を取り出すやり方が見つかりましたが、そのやり方だと View が所属している Scene をどうやって判断するかが課題になりそうです。


Discussion