UIWindowSceneを取得する際のアンチパターン
はじめに
iOS 13からUIWindowSceneが導入され、AppStoreのレビュー依頼を出す時など、UIWindowSceneが必要になることも増えました。
さて、このUIWindowSceneの取得方法ですが、よくStack Overflowなどで間違った実装を紹介されています。
この記事では、そのやり方を説明した後に、何が問題で、どうするべきなのか、を解説したいと思います。
よくある間違え
よくある間違えはこんな感じです。
let windowScene = UIApplication.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.first
UIApplicationからUISceneを取り出して、UIWindowSceneにキャストできたものを取り出すコードです。
あまり、Sceneについて詳しく人がこのコードを見ると、「なんかよくわからん」「でも手元で試したら、動いたぞ」となり、そのまま採用してしまう人も多いのではないでしょうか?
何が問題か?
一番わかりやすい問題は、iPadのマルチウインドウで正しく動かないことです。
connectedScenesの型はSet<UIScene>になっており、各ウインドウに対応するUIWindowSceneが入っています。
つまり、マルチウインドウの状況でユーザーが「ウインドウA」と「ウインドウB」を作った場合、このconnectedScenesにはウインドウAに関するUIWindowSceneとウインドウBに関するUIWindowSceneの二つが入っています。
// イメージ
connectedScenes = [
(ウインドウAのUIWindowScene),
(ウインドウBのUIWindowScene)
]
この状況で、最初に紹介した間違ったやり方をしてしまうと、ウインドウAのUIWindowSceneが欲しいのに、ウインドウBのUIWindowSceneを取得してしまう可能性があります。
中には「ユーザーが今操作しているUIWindowSceneが欲しい」と感じる人もいるかと思いますが、残念ながらそのような要求に応えるAPIは今のところ存在しません。
時々、以下のようにactivationStateを確認する実装を見かけますが、iPadで複数のウインドウが最前面の場合、複数のUIWindowSceneが同時にforegroundActiveになるケースは存在するので、この方法でも良くなさそうです。
let windowScene = UIApplication.shared
.connectedScenes
.compactMap { $0 as? UIWindowScene }
.filter { $0.activationState == .foregroundActive } // activationStateを確認
.first
ではiPadのマルチウインドウを諦めてしまえば、上のコードは問題ないのでしょうか?
答えはノーで、iPad以外でも上のコードは問題になるケースがあります。
iOS/iPadOSで外部ディスプレイに接続している場合、外部ディスプレイに表示されるUIは別のウインドウと認識され、UIWindowSceneが作成されます。
「マルチウインドウも外部ディスプレイもサポートしてないから、上のコードを使いたい」というのであれば使っても今のところ問題ないかもしれません。
ただ、結局のところ今のiOSの仕様の上でしか成り立たないハッキーな実装であり、今後のiOSの進化の際に正しく動かなくなることは十分に考えられますので、そのコストと見合っているのかを考える必要はあります。
ではどう取得するべきか?
一番シンプルな方法はUIViewから取得する方法です。
let windowScene = view.window?.windowScene
すごくシンプルです。
注意すべき点としては、対象のViewが表示されるまでwindowがnilになることです。
このwindowはviewWillAppearまではnilで、viewDidAppearもしくはXcode15以降ならviewIsAppearingのタイミングで値がセットされます。
別の方法としては、少し大変ですがSceneDelegateから対象のUIWindowSceneをパスしていく方法です。
SwiftUIを使っている場合、この方法+Environmentの組み合わせが良さそうです。
おわりに
広く紹介されているconnectedScenesは今のiOSの仕様でしか成り立たないハッキーな方法なので、避けられるなら避けた方が良いです。
UIViewからの取得が最も簡単ですが、アクセスするタイミングだけは気をつけた方が良いです。
Discussion