コンテナViewControllerについて
A-Liaison BLOG: Container View Controllerを作ってみよう
この記事の間違いが多すぎてやばいので。
iOS 5からはこのContainer View Controllerを自作する事が可能になりましたが、実装が面倒なのと大体の場合においてUIKitが用意しているContainer View Controllerを使うかcocoapodsあたりからそれっぽいライブラリを拾ってくれば解決するためかあまり具体的な実装方法が話題になっていないようです。
iOS5の頃は単に英語資料しか存在しなかったので、普通の人は存在を知らないし、存在を知っている人は英語をある程度読めるので、海外の記事や実装を直接読んでいただけじゃないのかなぁと思う。マジでスライドで概要を説明している人が1人いるだけとかそういうレベルだったんですよ…。
UIViewControllerプログラミングガイドに邦訳が載ったiOS6以降だと具体的な実装方法は日本語で読めるようになりましたし、実装方法を話題にしている人も圧倒的に増えたと思うんですけど…。
本題
didMoveToParentViewController:
とwillMoveToParentViewController:
の実装を間違える人は多いです。そもそもカスタムなコンテナUIViewController
の実装者の責務(must)であるにも関わらず、呼んでないコードを見たこともあります。
上の記事も間違えています。
しかしご安心ください。まず問題になりません。
なぜか。
全ての初期化処理を単一のライフサイクルメソッドに押し込める、viewDidLoad
病か、viewDidAppear
病の患者が極めて多く、didMoveToParentViewController:
のタイミングで何らかの処理を行うこと自体が極めて稀なためです。
独創的なUIViewController
のサブクラスを基底クラスとして案件ごとに用意して、独自の初期化処理を行うようなコードも見ます。回転系のライフサイクル系メソッドがバージョンによって変わったり、UITableViewController
を継承する必要が出てきて詰むやつです。
will/didMoveToParentViewController:とは
ViewControllerのライフサイクルメソッドのひとつです。
UIViewController
がネストする構造になっているときに、これから子として追加、もしくは親から削除されるタイミングで何らか処理を行いたい場合に記述できます。
引数のUIViewController
は親となるViewControllerです。nil
の場合、親から取り除かれることを意味しています。
何に使うのか
独自のコンテナViewControllerを作る場合、子から親へ何らかの通知を行いたい場合があります。
任意のタイミングでparentViewController
プロパティを利用し、直接親のViewControllerへアクセスすることは可能なのですが、何らかのプロトコルに準拠させて操作に制約を付けた方がスマートです。
そのような実装を考えたとき、いつdelegate
として親のViewControllerへの参照を保持/解放するかを考えると、親子関係が紐付く/解除されるこのタイミングが最も適切です。
addChildViewController:
を呼び出す親側で、delegate
の登録を行うことも当然できます。しかし親がインターフェースを規定してしまうので、子の実装に制約がかかります。
一方、子側でdelegate
の登録を行う場合は後からプロトコルを増やしたり、delegate
の登録を行わない特殊な子の存在を許容できたりと、柔軟性が大きく変わります。
ただしこれは、コンテナViewControllerが正しく実装されている という前提での話です。たとえばdidMoveToParentVieController:
を呼び出さないコンテナViewControllerでは、didMoveToParentVieController:
が呼び出されるのはViewControllerの階層から削除されるタイミングだけです。諦めましょう。
コンテナViewController実装者の責務
2年くらい前に書いた記事のコピペですけど、
addChildViewController:
の後にdidMoveToParentVieController:
removeFromParentViewController:
の前にwillMoveToParentViewController:
を、それぞれ手動で呼び出す必要があります。一方で、
addChildViewController:
の前のwillMoveToParentViewController:
removeFromParentViewController:
の後のdidMoveToParentViewController:
は、デフォルトでは自動的に呼ばれますので、何もしなくていいです。
なぜ手動で呼び出す必要があるメソッドと、そうでないメソッドがあるのでしょう?
答えは簡単で、ViewController追加/削除のトランジションがいつ始まり、いつ終わるのかはシステムには分からないからです。
addChildViewController:
が呼び出されたということは、これからViewControllerのトランジションが始まるのだということはシステムにも分かります。そこでwillMoveToParentViewController:
は自動的に呼び出されます。コンテナ実装者はトランジションが完了したタイミングで、didMoveToParentVieController:
をコールする必要があります。特にアニメーションなどない場合は即座に呼び出して問題ありません。
removeFromParentViewController:
の考え方も同様です。removeFromParentViewController:
が呼び出されるタイミングは既にViewControllerのトランジションが終了し、ViewControllerの階層から取り除かれた後なので、didMoveToParentViewController:
は自動的に呼び出してくれます。しかしトランジションが始まるタイミングはシステムには分からないので、コンテナ実装者はnil
を引数にしてwillMoveToParentViewController:
を呼び出す必要があります。
begin/endAppearanceTransition:animated:について
呼び出さなくていいです。
なぜ存在するのか
iOS5ではautomaticallyForwardAppearanceAndRotationMethodsToChildViewControllers
、iOS6以降はshouldAutomaticallyForwardAppearanceMethods
というBOOL
を返すメソッドがあり、コンテナViewController実装者は必要に応じてこのオーバーライドすることができます。
デフォルトはYES
ですが、NO
にすることで
viewWill/DidAppear:animated
viewWill/DidDisappear:animated
willRotateToInterfaceOrientation:duration:
など回転系のメソッド(バージョンによって違いがありすぎるので割愛)
などのトランジションが発生した際の通知を、子ViewControllerに伝播させないようにできます。(逆に言えば、通常は自動的に子に伝播されます)
親と子とでトランジションのタイミングを変えたい場合、または複数の子ViewControllerにディレイを掛けてトランジションさせたい場合など、子ViewControllerの外観の制御を親が完全にコントロールしたい場合のみ利用する特別なオプションです。まず考慮に入れなくていいので、確かに記憶から抹消しても構いません。
ただ以下のことを知らずにコンテナViewControllerを実装するのは論外です。
このとき、自動的に呼び出されなくなったwillRotateToInterfaceOrientation:duration:
などはコンテナViewControllerが任意のタイミングで、手動で呼び出すことができます。
しかし、viewWill/DidAppear:animated
およびviewWill/DidDisappear:animated
については、絶対に手動で呼び出してはいけません。これらのメソッドを呼び出す特権は、システムのみが持ちます。
そこでこれらのメソッドを直接呼び出す代替として、begin/endAppearanceTransition:animated:
を経由して通知を行うのです。
ですので、shouldAutomaticallyForwardAppearanceMethods
を記憶から抹消する場合、beginAppearanceTransition:animated:
の存在も一緒に抹消してください。
shouldAutomaticallyForwardAppearanceMethods
としてYES
を返す場合(つまりデフォルト)では、トランジションの開始と終了の通知は、will/didMoveToParentViewController:
を適切なタイミングで呼び出すだけで構わないのです。
余談
iOS5でviewDidAppear:内でaddChildViewController:するとChildViewControllerのviewWillAppear:が呼ばれない - Qiita
shouldAutomaticallyForwardAppearanceMethods
メソッドの命名から考えると、外観変更系のメソッドは「子に転送される」のでiOS5の実装が正しいような。
viewDidAppear
でaddChildViewController:
するということは、既に終了している親のviewWillAppear:
を子側へ転送するタイミングがないわけで、これでちゃんと呼び出してくれるiOS6以降がイレギュラーな感じ。