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