iPhone Xが発売[※1]されると共に導入されたノッチは、当初はiPhone Xのみへの対応を考えれば済んだのでしたが、徐々に追加されるノッチ付き端末は、極薄ベゼルが齎らす角丸表示と併せ、iPhoneアプリ作成に於いても充分に考慮せざるを得なくなりました。此れに対応する手法をApple社が開発環境に於いて用意したものがセーフエリアなる概念です。セーフエリアの中にアプリの必要事項を表示させれば、ノッチと角丸の欠け分に掛からずして、アプリ内容が不可視となってユーザーに不便を掛けないようにと配慮された手法です。
セーフエリアの取得
当初はセーフエリア外の余白を零と考えた四角表示領域の従来iPhoneと、iPhone Xのセーフエリア一種類だけと考えれば、iPhone Xか否かで分岐処理をすれば済み、セーフエリアも定数で済んだものが、年毎に追加されるノッチ端末毎にサイズが異なれば、セーフエリア外の余白の値は増えるばかりにて、当然ながら端末毎、其々のセーフエリア数値に対応する必要が出来したのです。此方もApple社が対応するにセーフエリア値取得用に用意したプロパティが「safeAreaInsets」にて以下の如く記述すれば端末から上下左右の余白を取得出来ると言うものでした。
Apple社の開発者用サイトには「Instance Property」としての当該案内ページ「safeAreaInsets」が用意されています。当該プロパティを用いれば得られた余白の値の余裕分、端末のエッジから離してアプリの内容を表示すれば宜しくなります。また、時計やWi-Fiやバッテリー状況を示すアイコンの乗るステータスバーは、従来のものは従来通り、ノッチ端末に於いてはノッチの左右にiPhoneが自動で良しなに配置してくれます。
ゲーム画面に於けるセーフエリア
開発機:MacBook Air M1 2020
MacOSバージョン:macOS Monterey 12.3
Xcodeバージョン:13.3
言語:Swift
主関連アプリ:うさ犬が行く
肝煎りApple側が斯くの如き用意をしてくれたとしても中々然うは上手く運ばないのがアプリ開発の常です。余白を避けてアプリを表示すれば、従来の端末では問題は無いもののノッチ端末に於いてはアプリに仍っては聊か間抜けた感じが否めなくなるものも有ります。特にゲームなどに於いてはビジュアル上、ゲーム画面を端末の幅いっぱいに、角丸の隅まで取りたく、すると余白に迄ゲーム画面を飛び出させて画像など表示したい要請が如何しても出て来ます。
ついては此の画面いっぱいに背景画像等を表示するに特段問題は有りません。従来通り、端末サイズにアスペクト等考慮しながらフィットさせれば宜しいだけです。しかしU
SpriteKitに於けるゲーム画面生成のタイミング
此処でUIViewControllerに於ける所与の各メソッドの表示順、即ちライフサイクルを考えてみましょう。UIViewControllerとはアプリの画面表示の為のクラスにて、当該UIViewControllerの表示前、表示後、非表示直前、解放時など様々なタイミングで呼び出されるメソッドが用意されています。アプリの表示はUIViewControllerの孰れかのメソッドに於いて其の呼び出されるタイミングを鑑みて表示の内容を記述しておくのでした。
さて、Apple社が提供する2次元ゲーム構築用フレームワーク「SpriteKit」を利用する場合もゲーム画面を表示するにはUIViewControllerを用意する必要が有りました。一般的にはビューが呼び出されて直後に呼ばれるメソッド「viewDidLoad」に「SKScene」クラスからビューインスタンスを生成し、此れと現在のビューと差し替える、と言った塩梅です。コードに起こすと以下の如くなるでしょう。
override func viewDidLoad() {
super.viewDidLoad()
let scene = GameScene() // SKSceneを継承したクラス
let skView = SKView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
self.view = skView
skView.ignoresSiblingOrder = true
scene.scaleMode = .aspectFill
scene.size = skView.frame.size
skView.presentScene(scene)
}
此のコードに有る様にビューの原点は「CGRect」メソッドに於いて「(0, 0)」で与えており、幅は端末幅「self.view.frame.width」で、高さは端末高さの「self.view.frame.height」で与えています。従って画面いっぱいにゲーム画面が表示される勘定です、従来のノッチを勘案しない端末ならば。しかし一旦ノッチが登場したからには此処でセーフエリアを取得して此の「SKScene」クラスから生成するビューインスタンスに適用しなければならなくなりました。するとゲームビューインスタンス以前にはセーフエリアを取得しなければなりません。
其処で「viewDidLoad」メソッド内で此の直前にセーフエリアを取得してみます。すると上下左右共に零が返されます。従来の端末ならば其の通りなのですが、ノッチ端末となれば話が違います。では、此のメソッド「viewDidLoad」はUIViewControllerのライフサイクルの那辺に呼び出されるのでしょうか。
UIViewControllerクラス所与のメソッドのライフサイクル
以下にUIViewController代表的な所与のメソッドを其の呼び出される順番に箇条書きしてみましょう。
- loadView:画面を表示する時
- viewDidLoad:画面を表示する時
- viewWillAppear:画面を表示する時
- viewWillLayoutSubviews:画面レイアウト構成時
- viewDidLayoutSubviews:画面レイアウト構成時
- viewDidAppear:画面を表示する時
- viewDisappear:画面を離れる時
- viewDidDisappear:画面を離れる時
「viewDidLoad」メソッドは可成り早い段階で呼び出されるのが分かります。しかし取得してみれば零が返されますから、此の時点でセーフエリアの値はシステム上決定していないことになります。
するとセーフエリアの値はどのタイミングで取得出来るのでしょうか。情報を求めて「safeAreaInsets」を見ても、どのタイミングで取得可能となるかなどと言う中々にクリティカルな情報は読み取り難くあるのは、アプリ毎に異なる環境下に特定のメソッドで取得しろなどという説明が出来難い事情も汲み取れるからには致し方有りません。しかし解決は簡単です。孰れもUIViewController上の所与のメソッドですから其々に試しに取得してコンソールに出力して確認してやれば宜しいでしょう。其の結果、得られたメソッドは「viewWillLayoutSubviews」でした。此のメソッドが呼び出されるタイミングに於いて、セーフエリアが零でない場合の値はシステム上明確になっているのが分かりました。此れと「viewDidLayoutSubviews」メソッドはレイアウトを確定させた前後に呼び出されるのは命名上からも明白です。Apple社がセーフエリアをレイアウト上のプロパティを位置付けているのも汲み取れます。
ゲーム画面インスタンス生成のタイミング
「viewWillLayoutSubviews」メソッド以後のUIViewControllerクラス所与のメソッドに於いてはセーフエリアの取得が可能となるのが分かりましたから、「viewDidLoad」ではなく「viewWillLayoutSubviews」メソッド内に当該記述を書き込んでやれば宜しいことになります。実際、ゲーム画面のビューインスタンスはviewWillLayoutSubviewsメソッド内でSKSceneクラスから生成すれば宜しい旨の情報も有ります。
果たして其の様な実装が可能なのか、試してみれば実際にゲーム画面が生成されます。従って此れからのSpriteKitを用いたゲームに於いての実装は古い定番情報「viewDidLoad」でシーン生成する方法から「viewWillLayoutSubviews」のものに置き換わって行くのでしょう。新規にゲームプロジェクトを起こす場合も此の実装を以て検証しつつ推し進めれば宜しいでしょう。処でセーフエリア登場以前から運営していたゲームに於いては如何でしょうか。中々に一筋縄には行かない様で、シーン生成を書き込むメソッドを変更してアップデートすると上手く機能しない場合が有る様です。手元の開発環境に於いてはゲーム音発声に於いて、具体的には「SKScene」クラス内の実装としての「AVAudioPlayerDelegate:AVAudioPlayer」にてエラーが発生してクラッシュの憂き目を見ては、シーン生成を記述するメソッドの交換の手法は採用出来ない羽目に陥りました。
裏技閃く
ゲームに音声は欠かせません。無音のゲームにする訳にも行かず、ほとほと困り果てました。ゲーム画面を表示するのを「viewDidLoad」で実行すれば問題は出ませんが、セーフエリアが此の時点ではノッチ端末に於いてさえ零としか返してくれませんので、UI部品を配置する基準が取れません。今後、セーフエリアが突っ外れた値を出して来ることもないでしょうから適当にセーフエリアを鑑みた配置をすれば従来の非ノッチ端末ではUI部品が中央によった間抜けな印象を免れません。如何にかして「viewDidLoad」内で、しかも生成された「SKScene」クラス内でセーフエリアを取得してUI部品の配置に寄与させたいのです。
音声発声の実装を最初から様子を見ながら描き直すか如何か困惑しつつ悩みながら、他に方法は無いか思案を巡らしていた処、「SKScene」クラス内の実装を見て不図思い付いた発想が有りました。通常「SKScene」クラスの初期化は「didMove」メソッドが担いますので、UI部品の配置は此のメソッド内に記述します。しかし此れは初期化時に実行されるメソッドであれば、「viewDidLoad」実行のタイミングに従うものであり、実はセーフエリアの値は取得不能です。では遅れて実行される「SKScene」クラスのメソッドは無かったか、斯うした経緯で思い至ったのが「SKScene」クラス所与の「update」メソッドです。
SKSceneクラスのupdateメソッド
Apple社の開発者用サイト「update(_:)」ページの説明を見れば「システムによってフレームごとに1回だけ呼び出され」ると説明されています。ゲームの進行は此のメソッドに負う状況が多く、するとゲームの初期画面が表示され、用意が全て整った後にゲームの終了迄、逐次実行される理屈です。然うとなれば「update」メソッド内でならば零ならぬ端末の持つ其々のセーフエリアの値を取得出来るのでは無いか、と言う発想です。
勿論、単に取得するコードを書けば、フレーム毎に呼び出されてしまいますから、例えば30FPSであれば1秒間に30回、同じセーフエリアを取得してゲーム終了まで繰り返すとんでもないコードになってしまいます。此れを回避する為には、フラグを設けて最初の1フレームのみ、取得コードを実行させる必要が有るでしょう。セーフエリアの値は一旦取得すればゲーム終了迄、再度の取得は全く必要有りません。此のフラグの設定も併せて「update」メソッド内にセーフエリアを取得してコンソールログを出力する試験的なコードを書いて実行した処、目論見通りの結果を得られたのでした。コードの大凡は以下の如くなります。赤字の変数「updateIni」はBool型に真として宣言しておいた変数です。
override func update(_ currentTime: TimeInterval) {
if updateIni {
paddingTop = (self.view?.safeAreaInsets.top)!
※UI部品の座標に得られた余白を宛行う処理
updateIni = false
}
…
}
理屈の上では、一旦は余白を無視して配置されたUI部品がゲーム開始の次の瞬間に余白を考慮した位置に再配置されるので一瞬移動が発生しますが、其処は其れ、人の目で追える範疇には有りません。さも最初から余白を考慮して配置されたようにUI部品達は振る舞うのです。
まとめ
今回採用した手法はシステムが1フレーム進める毎に必要の無いフラグ判定をさせますから聊か富豪的な感は否めぬものの次善の策として致し方無しとしましたが、矢張り本来はレイアウト上のプロパティとして画面配置は当該メソッドに従ったタイミングで実行した方が宜しいとは思います。従って決してお薦めの方法とは言えず、標題にも裏技的な扱いに留どめおいたものです。
アプリの画面構成は単純に一つのUIViewControllerで済ませられれば好いのですが、然う許りは行かない場合も多くあります。例えば画面を二つに区切って片方は固定し、片方はUINavigationControllerで遷移させる、などは極々普通に行われている実装でしょう。此の如く複数のUIViewControllerの表示順、上下を考え併せれば更に状況は複雑となります。今回は本記事に紹介した手法で首尾良く実装が叶いましたが、いつも然うと運ぶ許りにも行かないでしょう。
開発環境提供側にも色々事情は有るのは分かりますが、アプリ開発者側としては成る丈リソースの割かれる状況は避けたいものです。直感的操作を以て鳴らすApple社であるのならばセーフエリアを取得するに於いては、叶うものならば、アプリ起動時に呼び出されるクラスである「AppDelegate」にて取得可能にして貰えれば、此れが本来の此の値の好適な取得タイミングなのではないかと言う思いは拭い去れなくあるでした。
- iPhone XR発売直前なので一年前ドコモでiPhone Xを発売当日入手した経緯を記し置く(2018年10月9日)
バージョン: 3.21
リリース: 2015年9月14日
更新: 2022年3月7日
サイズ : 19.7 MB
互換性: iOS 14.4 以降のiPhone、iPod touch に対応。および、macOS 11.0以降とApple M1 チップを搭載したMac に対応。