なるようになるかも

力は多くの場合、その人の思いを超えない。

コンテナ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ではautomaticallyForwardAppearanceAndRotationMethodsToChildViewControllersiOS6以降は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の実装が正しいような。

viewDidAppearaddChildViewController:するということは、既に終了している親のviewWillAppear:を子側へ転送するタイミングがないわけで、これでちゃんと呼び出してくれるiOS6以降がイレギュラーな感じ。