[SwiftUI]UIWindowを取得する方法
iOSでSwiftUIを使って開発していても、UIWindowにアクセスする必要が出て来るケースがあります。例えば、AuthenticationServicesフレームワークなどです。
UIWindowにアクセスする方法はいくつかあるのですが、よりシンプルでスマートな方法を調べてみました。
- SwiftUI Lifecycleを使う
- Multi windowに対応する
環境
Xcode: 13.4
iOS: 15以降
実装方法
-
UIApplicationDelegateAdapterを設定して自作のAppDelegateを利用する -
AppDelegateで自作のSceneDelegateを利用するように設定する -
SceneDelegate内でUIWindowSceneからkeyWindow(UIWindow) を取り出す -
SceneDelegateをObservableObjectに適合させる - 使用するViewで
EnvironmentObjectとしてSceneDelegateを定義する
UIApplicationDelegateAdapterについては、以下の公式ドキュメントを参考にしています。
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が使えます。
理由は、公式ドキュメントに書いてあります。
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:
つまり、もし上記のAppDelegateをObservableObjectに適合すれば、UIApplicationDelegateAdaptorさえ宣言すると、追加実装なしでAppDelegateがEnvironmentObjectとして取り出せます。
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.
つまり、もし上記のSceneDelegateをObservableObjectに適合すれば、UIApplicationDelegateAdaptorさえ宣言すると、追加実装なしでSceneDelegateがEnvironmentObjectとして取り出せます。
SceneDelegate内のUIWindowは、それぞれのWindowごとに保持されるので、Multi windowsにも対応しており、Viewが所属するUIWindowが取得できます。
参考
Discussion