前回iOSアプリの基本構造という記事を書いたのですが、
長くなりすぎて、UIScreenとUIResponderについても入れたかったのですが、割愛しました。
今回はUIScreenについて書きます。
画面のサイズUIScreenからとるか?viewからとるか?
端末の画面サイズがコードの中で欲しいとき、ありますよね。
ViewControllerの中なら、
view.bounds
でとれますが、viewプロパティ持ってないクラスだと、この手はつかえません。
あと参照しているViewが画面全体を指定してないと、思ってたのと違うことになります。
色々やりようはあると思いますが、僕は
UIScreen.main.bounds
でとっていました。
あるいは、
view.window?.bounds
と指定する方法もありますね。
これだとUIViewが所属するUIWindowの領域がとれます。
コード的には、どれでもいいっちゃどれでもいいのですが、自分の中でもやもやしていたところなので、調べてみました。
UIScreen/UIWindow/UIView
ところで、appDelegateの中に、windowプロパティがあって、UIWindowの参照を持ってますね?
これでも画面サイズは取得することができます。
UIApplication.shared.keyWindow?.bounds
つまり端末の画面サイズをとる選択肢としては、3つあるわけです。
- UIScreen
- UIWindow
- UIView
3番目のUIViewからとるケースは、UIViewが端末サイズとイコールで定義されている場合限定なので、使える状況は限られます。
なので、UIScreenとUIWindow、どっちを使うのがええんや? というのが問題なわけです。
UIScreenとUIWindowの違い
端末サイズをとりたい、という目的だけで見ると、UIScreenとUIWindowは似たようなもんに見えるのですが、
ちゃんとドキュメント読んでみたら全然違うクラスでした。
UIScreenとは
UIScreenは、「ハードウェアの画面を抽象化したクラス」です。
iPhoneで言えばiPhoneの画面を表現していますし、AppleTVで言えばテレビ画面を表現しています。
iOSアプリで、最低1つ持つ必要があります。
つまりUIScreenは、iOSアプリが表示したいもの(What)を表しているのではなくて、表示する場所(Where)を表現しています。
通常のアプリであれば、UIScreen = 今アプリが稼働している端末の画面という理解でいいのですが、問題はミラーリングするようなアプリの場合です。
その場合、UIScreenはメインとサブが存在することになります。
UIWindowとは
一方、UIWindowは、iOSアプリが表示したいもの(What)を表します。
ただUIWindowそのものはユーザーの目に見えるコンテンツを直接配置する場所ではなく、
具体的なコンテンツはUIViewに乗せて、そのUIViewを乗せる場所がUIWindowです。
いわば台紙ですね。
UIWindowの取得方法ですが、「UIApplication.shared.keyWindow?」や「view.window」のwindowが実際に入るのが、
ViewDidAppear後らしいので、ViewControllerのタイミングによっては、とれないみたいです。
UIViewControllerのview.windowがロードされるタイミング
となると、appDelegate経由になるんで、下記でとらなきゃですね。
UIApplication.shared.delegate?.window
余談ですが、これでbounds取得しようとすると、
let window = UIApplication.shared.delegate?.window //UIWindow??型が返る
print(window!?.bounds) //Optional((0.0, 0.0, 375.0, 812.0))
となって、ちょっとビックリしました。
ベストプラクティスは?
というわけで、マルチスクリーンでないのであれば、UIScreenでとろうが、UIWindowでとろうが、返ってくる値としては一緒なのですが、
iPadに対応したアプリケーションを作る場合、マルチタスク機能が存在し、ScreenサイズとWindowサイズが等しくない場合があるため、アプリの表示領域のサイズを知りたい場合はWindowサイズを取得すると良い。
アプリの表示領域のサイズを取得する
という注意点もあるみたいです。
まとめると、画面サイズを取得する際にどのクラス使うかの選択基準は、こんな感じですかね。
- UIScreenを使う→マルチスクリーンやマルチタスクを考えなくてよい
- UIWindow(not Application Delegate)を使う→ViewControllerのライフサイクルを考えなくてよい
- UIWindow(Application Delegate)を使う→特に考慮点が思いつかない……あれ? もしかして万能?→いやごめんなさい。UIWindow??型なので扱いがめんどくさいですねこいつ
「今アプリが稼働している端末の画面サイズが正確に欲しい」という意味合いでは、原理主義的に考えたら、AppDelegateでとるのが筋なのかな〜と思うものの、
UIScreenでとるのは楽ですし、ググったらすぐ出てくるし、ほとんどのケースで正しい値がとれるので、
マルチスクリーン対応するようなことが想定されるアプリ(動画系とか)でなければ、UIScreenでやっちゃっていい気がします。
それぞれのクラスの違いを認識して、ケースバイケースで使っていくしかなさげです。
(おまけ)Application Delegateのwindowが2重オプショナル値返す件
この件、Swift書いていてもあんまり遭遇しないケースだったので、調べてみたら、StackOverflowで質問投げてる人がいました。
Why is main window of type double optional?
UIKitフレームワークの中のイマイチなトコなので、ユーザー的にはどうしようもないですが、
let window = app.delegate?.window??.`self`()
とすることで、UIWindow?型にできます。
これはちょっとあまりエレガントではないので、
let window = app.delegate?.window ?? nil // UIWindow?
こっちにした方がいいんじゃね? という指摘が飛んでいます。
いずれにせよちょっと扱いづらいですね。
(おまけ2)UIDevice
これを書いてしばらく経って急にそういえばUIDeviceってあったなと思い出しました。
ただこのクラスは端末サイズはプロパティとして持ってませんでした。残念。