🚨

UIWindowSceneを取得する際のアンチパターン

2023/08/16に公開

はじめに

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 = [
    (ウインドウAUIWindowScene), 
    (ウインドウBUIWindowScene)
]

この状況で、最初に紹介した間違ったやり方をしてしまうと、ウインドウ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が表示されるまでwindownilになることです。
このwindowviewWillAppearまではnilで、viewDidAppearもしくはXcode15以降ならviewIsAppearingのタイミングで値がセットされます。

別の方法としては、少し大変ですがSceneDelegateから対象のUIWindowSceneをパスしていく方法です。
SwiftUIを使っている場合、この方法+Environmentの組み合わせが良さそうです。

おわりに

広く紹介されているconnectedScenesは今のiOSの仕様でしか成り立たないハッキーな方法なので、避けられるなら避けた方が良いです。
UIViewからの取得が最も簡単ですが、アクセスするタイミングだけは気をつけた方が良いです。

Discussion