UIKitにおけるViewの表示ライフサイクルを調べたのでまとめます。
- いつ、どのようにViewが表示されるのか
- Viewのframeはいつ決定するのか
- ViewのConstraintはどのように変更され、反映されるのか
etc...
UI実装時のよくある課題
- なんかスクロールがカクカクする、引っかかる
- AutoLayoutが効かない
- Viewの表示位置がおかしい、表示されない
- 画面表示までが遅い
このような課題は、Viewの表示ライフサイクルを理解せずに開発していることが原因かもしれません。
よくあるUI実装でのデバッグ
UI更新の処理をサブスレッドで実行していたため、反映されていなかった。
⇒メインスレッドで実行するように変更する。
こちらはXcode9の新機能、Main Thread Checkerでワーニング表示できるようになるようです。
Main Thread Checker | Apple Developer Documentation
何かよくわからないけど、layoutIfNeededを書いたら直る
cell.contentView.frame.size.width = width
cell.contentView.setNeedsLayout()
cell.contentView.layoutIfNeeded()
View表示のための大まかな流れ
- Viewの読み込み
- 制約の追加(AutoLayout)
- 制約を元にViewのframeを計算(レイアウト)
- frameの位置に描画(レンダリング)
サンプルプロジェクトをつくって処理を追ってみる
試しに、以下のような3枚のビューを持つプロジェクトをつくって、表示処理をログ出力してみます。
サンプルプロジェクトはこちら
https://github.com/shtnkgm/ViewLifecycleSample
ビューは下から順に、以下のカスタムクラスを実装しています。
- WhiteView (self.view)
- RedView (WhiteViewのsubview)
- BlueView (RedViewのsubview)
ログの出力方法
以下のようにクラス名とメソッド名を各メソッド呼び出し時にログ出力します。
ログの出力方法についてはこちらに詳細を記載しています。
クラス名や関数名等をログ出力する方法
View表示までのログの出力結果
サンプルプロジェクトを実行し、起動からViewの表示までのログを表示すると以下のようになります。
さっくりと、先ほど説明した大まかな流れになっていることが、各メソッド名から予想できます。
各メソッド名の説明
Viewの表示ライフサイクルにおける、各メソッドの説明を順にします。
UIView編
制約の更新に関するメソッド
updateConstraints()
- 制約の更新を実行
- 開発者が直接呼び出すのはNG
setNeedsUpdateConstraints()
- 制約更新の実行要否のフラグを立てる
- 計算実行タイミングはシステム任せ
updateConstraintsIfNeeded()
- 制約更新を即座に実行(更新フラグあれば)
パフォーマンスの良い順
- overrideしたupdateConstraints()内で制約更新
(レイアウトエンジンのバッチ処理に含まれ、メインスレッドをブロックしない) - 制約更新後にsetNeedsUpdateConstraints()
(エンジンがまとめて更新処理を実施) - 制約更新後にupdateConstraintsIfNeeded()
(即時実行のため、バッチ処理に含まれない)
レイアウトに関するメソッド
layoutSubviews()
- frameの更新を実行
- 開発者が直接呼び出すのはNG
setNeedsLayout()
- frame更新要否のフラグを立てる
- 計算実行タイミングはシステム任せ
layoutIfNeeded()
- frame更新を即座に実行(更新フラグあれば)
パフォーマンスの良い順
- overrideしたlayoutSubviews()内でframe更新
(レイアウトエンジンのバッチ処理に含まれ、メインスレッドをブロックしない) - frame更新後にsetNeedsLayout()
(エンジンがまとめて更新処理を実施) - frame更新後にlayoutIfNeeded()
(即時実行のため、バッチ処理に含まれない)
描画に関するメソッド
draw()
- 開発者が直接呼び出してはいけない
- CoreGraphicsを使って画面に描画する
setNeedsDisplay()
- 描画更新の実行要否のフラグを立てる
- 制約やレイアウト更新のように、 即時実行用のメソッドはなし
ビュー階層と実行順序
以下の通り、制約とレイアウトの更新メソッドはビューの階層構造に関して実行順序が異なります。
UIViewController編
loadView()
- 管理するViewを読み込む(self.view)
- StoryBoardで実装する場合はoverride不要
- Viewの追加、制約の追加など、StoryBoardで行う操作をコードで実装するのに適する
viewDidLoad()
- loadView()が完了した際に呼ばれる
- VCの表示サイクルで一度だけ呼ばれるため、 クラス内で利用するオブジェクトの初期化などに適する
viewWillAppear()
- ビューが表示される直前に呼ばれる
- 初回表示以外にもバックグラウンド復帰、タブ切り替えなど
- まだビューが表示されていないため、計算コストの高い処理は避ける
updateViewConstraints()
- サブビューの制約更新後、self.viewの制約更新が必要な際に呼ばれる
- self.viewのupdateConstraints()が呼ばれる
- overrideでの利用用途はあまりないのかもしれません
viewWillLayoutSubviews()
- ビューのレイアウトを開始する直前に呼ばれる (初期表示時や画面回転時など)
- ビューのlayoutSubviews()が実行される
viewDidLayoutSubviews()
- ビューのレイアウトが完了した際に呼ばれる (複数回呼ばれるので、オブジェクトの初期化などには向かない)
- self.view.frameはこのメソッドよりも前だと確定していない
- viewDidLoad()などでself.view.frameを用いてレイアウトすると意図するレイアウトとならない可能性あり
viewDidAppear()
- ビューが表示された直後に呼ばれる
- viewWillAppear()同様、バックグラウンド復帰時やタブ切り替え時など複数回呼ばれる
- 既にUI表示が完了しているので、UI表示に関係のない処理を実行するのに適する(ログ送信など)
制約を更新したらどうなる
サンプルプロジェクトの赤いビューと青いビューの制約を更新してみました。
関連する親ビューのsetNeedsLayoutが呼ばれることがわかります。
更新フラグが立つことで、layoutSubviews()が呼ばれます。(レイアウトエンジンの更新タイミング)
UI実装におけるデバッグ例
今回の内容に関連して、UI実装におけるデバッグ例のQAをご紹介します。
AutoLayoutでの アニメーションが動かない
- frameでのアニメーションと違い、 ブロック外に更新処理を書く
- アニメーションブロック内で、layoutIfNeeded()を実行することで、frameが決定します。
コードで制約を追加したら、 コンフリクトした
- AutoResizingMask由来の 制約が混在している状態
UIScrollViewやUICollectionViewなどのスクロールがカクつく
- scrollViewDidScroll()などでレイアウト更新を即時実行してないかチェック
- ビューのレイアウト調整がメインスレッドをブロックしてしまい、スクロールのためのレイアウト調整が止まっている可能性あり
- layoutSubviews()をoverrideすると良いかも