メモリ管理・レイアウトの観点からみた UIViewController の view の扱い
self.view
iOS 開発において、UIViewController の view の振る舞いは一番理解しておきたい点の一つです。
今回はその view に対して、メモリ管理とレイアウトの2つの視点を交えてアプローチを行い、
UIViewController の subclass を作成する上で、
UIViewController の各メソッドにどんな処理を書くべきか、そして何を書くべきでないか
を説明出来ればなと思っています。
iOS 6 以降からを対象として考えていますので、 iOS 5 以前は取り扱いません。
self.view の振る舞い
扱いを学ぶには、まず対象の振る舞いを把握する事からです。
ライフサイクルとレイアウトサイクルの2点から簡単に復習します。
self.view のライフサイクル
UIViewController の view がどのようなタイミングで生成されメモリ上に展開し、
どのようにメモリから消されていくか、を Apple が提供している図を元に簡単に把握しましょう。
では上記の要点を簡単に。
view はアクセスされたタイミングで初めて生成される
アプリケーションからビューを要求されてはじめて、UIViewController は view をメモリ上に展開し、
自身の view プロパティにも保持するようになります。
UIViewController の提供されているメソッドを用いたロードサイクルは上記の図の通りです。
システムの命令により、UIViewController は自身の view を window からアンロード
UIViewController は自身が破棄されるまでは retain 属性の view プロパティを保持し続けるのがデフォルトの動作です。
メモリ警告が起きると、以下のメソッドが呼ばれてシステムにより UIViewController の view が
何らかの理由で window にアタッチされている場合にはアンロードされてしまいます。
(2014/02/01 修正 - ブックマークコメントで指摘を頂いた箇所を修正しました。)
(2014/02/03 追加 - ブックマークコメントの詳しい修正を以下のブログに書いて頂きました。ありがとうございます! )
何らかの理由で 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