jarinosuke blog

about software engineering, mostly about iOS

メモリ管理・レイアウトの観点からみた UIViewController の view の扱い

self.view

iOS 開発において、UIViewController の view の振る舞いは一番理解しておきたい点の一つです。

今回はその view に対して、メモリ管理とレイアウトの2つの視点を交えてアプローチを行い、

UIViewController の subclass を作成する上で、

UIViewController の各メソッドにどんな処理を書くべきか、そして何を書くべきでない

を説明出来ればなと思っています。

iOS 6 以降からを対象として考えていますので、 iOS 5 以前は取り扱いません。

self.view の振る舞い

扱いを学ぶには、まず対象の振る舞いを把握する事からです。

ライフサイクルとレイアウトサイクルの2点から簡単に復習します。

self.view のライフサイクル

UIViewController の view がどのようなタイミングで生成されメモリ上に展開し、

どのようにメモリから消されていくか、を Apple が提供している図を元に簡単に把握しましょう。

https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/Art/loading_a_view_into_memory_2x.png

では上記の要点を簡単に。

view はアクセスされたタイミングで初めて生成される

アプリケーションからビューを要求されてはじめて、UIViewController は view をメモリ上に展開し、

自身の view プロパティにも保持するようになります。

UIViewController の提供されているメソッドを用いたロードサイクルは上記の図の通りです。

システムの命令により、UIViewController は自身の view を window からアンロード

UIViewController は自身が破棄されるまでは retain 属性の view プロパティを保持し続けるのがデフォルトの動作です。

メモリ警告が起きると、以下のメソッドが呼ばれてシステムにより UIViewController の view が

何らかの理由で window にアタッチされている場合にはアンロードされてしまいます。

(2014/02/01 修正 - ブックマークコメントで指摘を頂いた箇所を修正しました。)

(2014/02/03 追加 - ブックマークコメントの詳しい修正を以下のブログに書いて頂きました。ありがとうございます! )

Viewのリソースの話。

何らかの理由で window からデタッチされている場合にはアンロードされてしまいます。

- (void)didReceiveMemoryWarning;

詳しくは以下が参考になると思います。

viewDidUnloadがdeprecatedになった理由を考察

self.view のレイアウトサイクル

UIViewController の view の frame は、様々な外部要因によって決定されます。

簡単に例を挙げると以下の通りです。

UIWindow の rootViewController

  • 表示されている window の frame

  • ステータスバーの有無、通話中など Background Mode が作動時の高さの変更

  • デバイスの傾き

UITabBarController, UINavigationController などの Container ViewController

上記の Child ViewController は、親の Container ViewController の frame に従い設定されます。

開発者はこれらを意識したレイアウトの実装を UIViewController に行い、

それぞれに適したレイアウトを、 UIViewController の外部からは意識せずとも実現させるのが理想です。

では肝心の UIViewController は、上記の frame 決定プロセスにどのように関与できるのでしょうか?

大まかなメソッドが呼ばれる流れは以下の通りです。

簡略化のため、UIViewAutoresizing と Auto Layout は省いています。

///(1) self.view の frame がシステムによって変更される

///(2) 以下が呼ばれる
- (void)viewWillLayoutSubviews;

///(3) self.view の layoutSubviews が呼ばれる

///(4) 以下が呼ばれる
- (void)viewDidLayoutSubviews;

駆け足で UIViewController のメモリ管理とレイアウトの流れを復習しました。

では実際に、それらに関した処理をどのメソッドに書くべきかを見ていきましょう。

実装メソッド

- (id)init

initializer では、その UIViewController が適切な振る舞いを行うための全ての状態の設定を書くべきです。

具体的にはカレンダーの画面を表示する ViewController だったら NSCalendar、

メモ帳の詳細画面だったらメモの内容はここで設定するのが良いでしょう。

逆に、 view やそれの subview の初期化は行うべきではありません。理由は後述。

- (void)loadView

loadView はシステムによって UIViewController の view プロパティにアクセスされ、

まだ view プロパティが nil の場合に発火します。

override している loadView 内で

[super loadView];

を行うことで初めて view プロパティに何も設定されていない UIView が入ります。

ここでは self.view と、その subviews の load の一点だけを行うべきです。

self.view の frame を用いて、 subviews の frame 調整は行うべきではありません。理由は後述。

- (void):viewDidLoad

loadView が完了されると呼ばれるメソッドです。

名前の通り、UIViewController が管理する全ての view がロード済みであることが保証されています。

なので、ここでは view に対して初期化以外の処理を行うべきです。

具体的には view へのデータセットや Target-Action などの通知設定などがあたります。

- (void):viewDidLayoutSubviews

loadView の項で frame 調整は行うべきではない と書きましたが、それはここで行うべきだからです。

self.view の subviews.frame の調整、すなわちレイアウト処理は全てここで記述するべきです。

なぜなら、このメソッドが発火するタイミングは、self.view の layoutSubviews の後、

要するにシステムや Container ViewController による self.view の frame 調整の後だからです。

このメソッド内で得られる self.view.frame がそのまま画面上に描画されるので、

Navigation Bar があるとか、Tab Bar が無いとか、デバイスが横向きとか全てが反映された後の frame になっています。

- (void):didReceiveMemoryWarning

メモリ警告が発生した場合に呼ばれるメソッドです。

ここでは不要なプロパティや self.view を解放するべきです。

このメソッド内で self.view に nil をセットする事で、

次回以降この UIViewController の view にアクセスがあった際に loadView が発火します。

loadView では view の初期化処理のみを行うべきと書いたのはそれが理由です。

サンプル

最後に GoodViewController と BadViewController という名前でサンプルを書いてみました。

BadViewController は「べきでない」を実践している最悪な UIViewController、

そして GoodViewController は上記の「べき」を実践した UIViewController。

サンプルはあくまでイメージですので、動作や値の保証は致しません。

viewDidLoad 全記述原理主義を目撃した際は、このブログを教えてあげて下さい。

参考

view | UIViewController Class Reference

Resource Management in View Controllers | View Controller Programming Guide

Resizing the View Controllers’s Views | View Controller Programming Guide