SNOOZE LOG

iOS中心のプログラミング関連技術メモ


スポンサードリンク

AutoLayout時代のUIViewサブクラス作法


スポンサードリンク

細かな要件をスマートでコンパクトに満たすため、UIViewのカスタムクラスを作成したいと思いました。
UIViewには様々な描画サイクルに関連したメソッドがありますがどういう場合にoverrideし、どういうコードを記述すれば良いのでしょう。詳細な日本語の記事が無かったので調べてみました。

UIViewの描画サイクル3つのステップ

(1)制約の更新   AutoLayoutを使用している場合、制約の更新があれば設定する段階です。
(2)フレームの更新
更新された制約をもとにframeが設定される段階です。
最終的なframeに、手動レイアウトによるカスタマイズが可能です。
(3)レンダリング
CoreGraphicsフレームワークと関連。UIViewが持つCALayerをカスタムする際に使用します。
今回はレイアウトまわりの実装にフォーカスするため、割愛いたします。

制約の更新


// overrideポイント
override func updateConstraints() {
  /*
 ・・・
 制約の更新コードを記述
 ・・・
 */
  super.updateConstraints() // 最後に実行
  // !!self.setNeesdsUpdateConstraints()!!実行ループになってしまうので、ここで呼び出すのは間違いです
}


・自作のUIViewサブクラス(カスタムビュー)でオーバーライドが可能です。 ・主にサブクラス自身の制約の更新コードを記述します。
overrideしたら、最後にsuper.updateConstraints()を必ず呼ぶ必要があります。(ないとクラッシュします。)
なぜ最後に呼ぶかというと、ビューの階層とこのメソッドの呼び出し順序との関連があるからかと思います。
・呼び出したビューの最も深い階層のサブビューから、最上位のスーパービュー(=呼び出したビューまで)の順で実行されます。
・「制約に変更がなかった」「ビューが最上位にある」場合にupdateConstraints()は呼ばれないことがあります。
・もし、カスタムビューの制約プロパティが変更されていないときにも、updateConstraints()実行のスケジューリングしたい時は、

// システムによるupdateConstraints()実行の確実なスケジューリング
setNeedsUpdateConstraints()


を呼びだします。制約の更新が確実に行われたことが確認できます。
・カスタムビューが持つ制約プロパティに更新があれば、updateConstraints()の実行スケジューリングは自動的に行われます。
・setNeedsUpdateConstraints()は、updateConstraints()内で呼び出すと実行ループになってしまい、クラッシュしますので間違いです。あえてsetNeedsUpdateConstraints()をコールする場合は、そのviewを参照しているオブジェクトがsetNeedsUpdateConstraints()を呼び出すことが多いでしょう。
・setNeedsUpdateConstraints()は、
∟確実にupdateConstraints()の実行させる。
∟updateConstraints()で制約の更新以外の副次的な処理を実装している。 場合に有効かなと思いました。
・システムがレイアウトを更新するタイミングでカスタムビューに実装したupdateConstraints()が実行されるようになります。
例えばUIViewControllerのサイクルの通過時や、superviewからの更新呼び出し時です。

システムが行うより早くupdateConstraints()の実行したい場合は、

// 任意のタイミングでupdateConstraints()を実行
updateConstraintsIfNeeded()


をコールします。
・updateConstraints()は直接コールしないことが推奨されています。(いつでもシステムを通して同期をとり、効率的にビュー更新を行うためでしょう。)

どんな時にupdateConstraints()をオーバーライドするか

・自作のUIViewサブクラス自身が持っているsubviewの制約を変えたい時
AppleAPIリファレンスでは、

制約の変更が遅すぎる(重たい変更)場合
ビューが多数の冗長な変更を生成している場合
としています。それ以外では、制約の変更を行う場所で変更しましょう、と書かれてあります。
・変更が影響を受けた直後に制約を更新するほうが、よりクリーンで簡単です。たとえば、ボタンタップに応じて制約を変更する場合は、ボタンのアクションメソッドで直接変更を行います。
つまり制約の追加・削除は必ずupdateConstraints()内に記述しないといけない、ということではないです。
一度だけ実行されるような制約の追加は、イニシャライザとともに実行されるようにも記述しても問題ありません。
また、推奨として、
・すべての制約を無効にするような変更はせず、変更が必要な制約のみを変更するようにして、効率的な実装をしましょう。 とのこと。

参考:
updateConstraints() - UIView | Apple Developer Documentation

また、WWDCでは、以下のように言及されています。要約ですが、

・実際にはupdateConstraints()の実装が必要ではないことがよくあります。
・すべての初期制約設定は、理想的にはInterface Builder内で実行する必要があります。または、プログラムで制約を割り当てる必要があることが実際に分かった場合は、viewDidLoad方がはるかに優れています。
・実際には定期的に繰り返す必要がある変更を、記述するためのもの。
・描画エンジンがこのパスで発生するすべての制約の変更をバッチとして処理できるため updateConstraints内部での制約の変更は、他の時点での制約の変更よりも実際に速い。

参考:
How to Use updateConstraints – Ole Begemann

フレームの更新


// overrideポイント
override func layoutSubviews() {
   super.layoutSubviews()  // 最初に呼び出す
 /*
 ・・・
  サブビューのframeの操作などをここに記述
 ・・・
  */
}


・AutoLayoutを使用している場合はsuper.layoutSubviewsを最初に呼び出す必要があります。(しないとクラッシュする) super.layoutSubviewsを最初に呼び出すことによって、制約をもとにサブビューのframeが決定され、操作できるようになります。
・最上位のスーパービュー(呼び出したビュー自身)からそこに追加されているサブビューの順で、最下層のビューまで実行されます。
AutoLayoutを使用してないならば、デフォルトではlayoutSubviews()は何も行われません

// 通常のレイアウト更新の際にlayoutSubviews()が実行されるようにマーク
setNeedsLayout()  


でフラグを立てておき、通常のレイアウト更新の際にlayoutSubviews()が実行されます。
即座にlayoutSubviews()の実行したい場合は、

// 任意のタイミングでlayoutSubviews()を実行
layoutSubviewsIfNeeded()


のコールで間接的に実行します。システムによって、効率的なタイミングでlayoutSubviews()が実行されます。

どんな時にlayoutSubviews()をオーバーライドするか

・layoutSubviews時点では、すべての制約がframe値への置き換え計算が終わっているため、最終的なframeのアウトプットに手を加えたい時。
・手動Layout(frameを直接操作してレイアウトするような旧来の)での実装を行いたい場合。

APIリファレンスより

・デフォルト実装は、iOS 5.1以前では何も行いません。
・デフォルトの実装では、設定した制約を使用し、サブビューのサイズと位置が決定されます。 ・サブビューの自動サイズ調整および制約ベースの動作が必要な動作を提供しない場合にのみ、このメソッドをオーバーライドする必要があります。実装を使用して、サブビューのフレーム矩形を直接設定することができます。

参考:

layoutSubviews - UIView | Apple Developer Documentation

レンダリング

本記事ではレイアウトまわりに主眼を置いたため、割愛しました。

まとめ

・updateConstraints()・・・この中に制約の変更を記述することで、パフォーマンスの向上が期待できるが、書くとしたら定期的な制約の更新だろう。かなり重たい処理でなければ可読性の面から使わない方が良いことはAppleのリファレンスからわかった。例えば何らかのアクションがあった時にsetNeesdsUpdateConstraints()をコールし、離れた場所にあるupdateConstraints()を間接的に実行するよりも、制約の変更コードをそのままアクションが起こった場所(コールバック関数など)に記述することが推奨されていることがわかった。
・setNeedsUpdateConstraints()・・・updateConstraints()を後でシステムタイミングで実行したい時。
・updateConstraintsIfNeeded()・・・updateConstraints()を即時に実行したい時。
・layoutSubviews()・・・overrideすることで手動レイアウトでframeをカスタマイズ出来ることがわかった。
・setNeedsLayout()・・・layoutSubviews()を後でシステムタイミングで実行したい時。
・layoutSubviewsIfNeeded()・・・layoutSubviews()を即時に実行したい時。

UIViewのupdateConstraints()やlayoutSubviews()は、UIViewControllerのviewDidLoad()、viewWillAppear()と、雰囲気が似ている感じがしていましたが、単に実行タイミングだけの感覚でオーバーライドするメソッドとは少し性質の違うもののように思いました。もやもやしていたのが、少しはっきりしたように思います。

参考

UIViewControllerのライフサイクル - Qiita
iOSエンジニア必見!!iOSのレイアウトで押さえておきたいこと【総集編】 | eureka tech blog
詳解 iOS SDK 第4版 ―ワンランク上のiPhone/iPadプログラミング
他、本文中にURLを記載


スポンサードリンク