👏

[SwiftUI]UIWindowを取得する方法

2022/07/22に公開

iOSでSwiftUIを使って開発していても、UIWindowにアクセスする必要が出て来るケースがあります。例えば、AuthenticationServicesフレームワークなどです。
UIWindowにアクセスする方法はいくつかあるのですが、よりシンプルでスマートな方法を調べてみました。

  • SwiftUI Lifecycleを使う
  • Multi windowに対応する

環境

Xcode: 13.4
iOS: 15以降

実装方法

  • UIApplicationDelegateAdapterを設定して自作のAppDelegateを利用する
  • AppDelegateで自作のSceneDelegateを利用するように設定する
  • SceneDelegate内でUIWindowSceneからkeyWindow(UIWindow) を取り出す
  • SceneDelegateObservableObjectに適合させる
  • 使用するViewでEnvironmentObjectとしてSceneDelegateを定義する

UIApplicationDelegateAdapterについては、以下の公式ドキュメントを参考にしています。
https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor/

AppからSceneDelegateの実装までは下記になります。

@main
struct SwiftUIWindowSampleApp: 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
     }
    
}

これで準備は整いました。実際にView内で使用してみます。

struct ContentView: View {
    
    @EnvironmentObject var sceneDelegate: SceneDelegate
    
    var body: some View {
        Text("Hello, world!")
            .padding()
            .onAppear {
                debugPrint(sceneDelegate.window ?? "null")
            }
    }
}

@EnvironmentObjectとして、先に定義したSceneDelegateを取り出します。

特にContentViewに対して.environmentObject(_)でObservableObjectを渡さなくても、上記のようにSceneDelegateが使えます。
理由は、公式ドキュメントに書いてあります。

https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor/

If your app delegate conforms to the ObservableObject protocol, as in the example above, then SwiftUI puts the delegate it creates into the Environment. You can access the delegate from any scene or view in your app using the EnvironmentObject property wrapper:

つまり、もし上記のAppDelegateObservableObjectに適合すれば、UIApplicationDelegateAdaptorさえ宣言すると、追加実装なしでAppDelegateEnvironmentObjectとして取り出せます。

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.

つまり、もし上記のSceneDelegateObservableObjectに適合すれば、UIApplicationDelegateAdaptorさえ宣言すると、追加実装なしでSceneDelegateEnvironmentObjectとして取り出せます。

SceneDelegate内のUIWindowは、それぞれのWindowごとに保持されるので、Multi windowsにも対応しており、Viewが所属するUIWindowが取得できます。

参考

https://qiita.com/shiz/items/3b829b1521f9723aa875
https://zenn.dev/konomae/articles/55759a688a7dfc

Discussion