Container View Controller を実装する

Container View Controller は、複数の View Controller のコンテンツを 1 つのユーザーインターフェイスに結合する手段です。Container View Controller は、ほとんどの場合、ナビゲーションを容易にし、既存のコンテンツに基づいて新しいユーザーインターフェイスを作成するために使用されます。UIKit の Container View Controller の例として、UINavigationControllerUITabBarControllerUISplitViewController があります。これらはすべて、ユーザーインターフェイスの異なるパーツ間でナビゲーションしやすくします。

カスタムContainer View Controllerを設計する

Container View Controller は、ルートビューといくつかのコンテンツを管理するという点で、他のコンテンツ View Controller とほとんど同じです。異なるのは、Container View Controller がコンテンツのパーツを他の View Controller から取得するという点です。取得するコンテンツは、他の View Controller のビューに限られ、そのビューは自身のビュー階層内に埋め込まれます。Container View Controller は、埋め込まれたビューのサイズと位置を設定しますが、それらのビュー内のコンテンツは元の View Controller が引き続き管理します。

独自の Container View Controller を設計するときは、必ずコンテナとそれに含まれる View Controller の関係を理解してください。View Controller 間の関係は、コンテンツを画面に表示する方法と、コンテナが View Controller を内部で管理する方法を伝えるのに役立つことがあります。設計プロセスでは、次の質問を自問してください。

各種オブジェクトの役割を定義すると、Container View Controller の実装は比較的簡単です。UIKit により求められるのは、Container View Controller とすべての子 View Controller の正式な親子関係を確立することだけです。親子関係により、子は関連するシステムメッセージをすべて受け取るようになります。それとは別に、実際の作業の大部分は、含まれているビュー (コンテナごとに異なります) のレイアウトおよび管理中に行われます。ビューは、コンテナのコンテンツ領域のどこにでも配置でき、それらのビューは必要に応じてどのサイズにも設定できます。ビュー階層にカスタムビューを追加して、装飾を行ったり、ナビゲーションを容易にしたりすることもできます。

例:Navigation Controller

UINavigationController オブジェクトは、階層データセットを通じてナビゲーションをサポートします。ナビゲーションインターフェイスには、一度に 1 つの子 View Controller が表示されます。インターフェイス上部にあるナビゲーションバーには、データ階層における現在の位置が表示され、1 つ前のレベルに戻るための戻るボタンが表示されます。データ階層内へのナビゲーションは、子 View Controller の左にあり、テーブルまたはボタンを使用する場合があります。

View Controller 間のナビゲーションは、Navigation Controller とその子によりまとめて管理されます。ユーザーが子 View Controller のボタンまたはテーブル行を操作すると、子は Navigation Controller に新しい View Controller をビューにプッシュするように求めます。子は、新しい View Controller のコンテンツの設定を処理しますが、Navigation Controller はトランジションアニメーションを管理します。Navigation Controller は、最上位の View Controller を淡色表示にする戻るボタンが表示されるナビゲーションバーも管理します。

図 5-1」は、Navigation Controller とそのビューの構造を示しています。コンテンツ領域の大部分には、最上位の子 View Controller が表示され、ナビゲーションバーが表示される部分はわずかです。

図 5-1  ナビゲーションインターフェイスの構造

コンパクト環境と標準環境のどちらでも、Navigation Controller は子 View Controller を一度に 1 つだけ表示します。Navigation Controller は、利用可能なスペースに合わせてその子のサイズを変更します。

例:Split View Controller

UISplitViewController オブジェクトは、2 つの View Controller のコンテンツをマスタと詳細が対になった配置に表示します。この配置では、1 つの View Controller (マスタ) のコンテンツにより、他の View Controller が表示する詳細が決まります。2 つの View Controller の表示設定は設定可能ですが、現在の環境の影響も受けます。横方向の標準環境では、Split View Controller が両方の子 View Controller を横に並べて表示したり、マスタを非表示にして必要に応じて表示したりすることができます。コンパクト環境では、Split View Controller は View Controller を一度に 1 つだけ表示します。

図 5-2」は、横方向の標準環境における Split View インターフェイスとそのビューの構造を示しています。Split View Controller 自体には、デフォルトではコンテナビューしかありません。この例では、2 つの子ビューが横に並んで表示されます。子ビューのサイズは、マスタビューの表示設定のように設定可能です。

図 5-2  Split View インターフェイス

Interface Builder でコンテナを設定する

設計時に親子のコンテナ関係を作成するには、「図 5-3」に示すように、コンテナビューオブジェクトをストーリーボードシーンに追加します。コンテナビューオブジェクトは、子 View Controller のコンテンツを表示するプレースホルダオブジェクトです。コンテナ内の他のビューに対して子のルートビューをサイズ設定および配置するには、このビューを使用します。

図 5-3  Interface Builder でコンテナビューを追加する

1 つ以上のコンテナビューを持つ View Controller を読み込むと、それらのビューに関連付けられた子 View Controller も Interface Builder により読み込まれます。適切な親子関係が作成されるように、子は親と同時にインスタンス化される必要があります。

親子のコンテナ関係を設定するときに Interface Builder を使用しない場合、「“子 View Controller をコンテンツに追加する”」で説明されているように、子を Container View Controller に追加することでそれらの関係をプログラムにより作成する必要があります。

カスタム Container View Controller を実装する

Container View Controller を実装するには、View Controller とその子 View Controller の関係を確立する必要があります。これらの親子関係は、子 View Controller のビューを管理する前に確立する必要があります。そうすると、UIKit は View Controller が子のサイズと位置を管理していることを認識できます。これらの関係は、Interface Builder で作成するか、プログラムにより作成することができます。親子関係をプログラムにより作成する場合、子 View Controller を View Controller 設定の一部として明示的に追加および削除する必要があります。

子 View Controller をコンテンツに追加する

子 View Controller をコンテンツにプログラムにより組み込むには、次の手順に従って、関連する View Controller の間に親子関係を作成します。

  1. Container View Controller の addChildViewController: メソッドを呼び出します。

    このメソッドは、Container View Controller が子 View Controller のビューを管理するようになることを UIKit に伝えます。

  2. 子のルートビューをコンテナのビュー階層に追加します。

    子のフレームのサイズと位置は、必ずこのプロセスの一部として設定してください。

  3. 子のルートビューのサイズと位置を管理するための制約を追加します。

  4. 子 View Controller の didMoveToParentViewController: メソッドを呼び出します。

リスト 5-1」は、コンテナが子 View controller をそのコンテナに埋め込む方法を示しています。親子関係を確立すると、コンテナはその子のフレームを設定し、子のビューを自身のビュー階層に追加します。子のビューのフレームサイズを設定することは、ビューがコンテナに正しく表示されるために重要です。ビューを追加すると、コンテナが子の didMoveToParentViewController: メソッドを呼び出し、ビューの所有権の変更に対応するチャンスを子 View Controller に与えます。

リスト 5-1  子 View Controller をコンテナに追加する

- (void) displayContentController: (UIViewController*) content {
   [self addChildViewController:content];
   content.view.frame = [self frameForContentController];
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];
}

前の例では、子の didMoveToParentViewController: メソッドのみ呼び出す点に注意してください。これは、addChildViewController: メソッドが子の willMoveToParentViewController: メソッドを自動的に呼び出すためです。didMoveToParentViewController: メソッドを自分で呼び出す必要がある理由は、子のビューをコンテナのビュー階層に埋め込むまでこのメソッドが呼び出されないためです。

自動レイアウトを使用している場合、子をコンテナのビュー階層に追加した後、コンテナと子の間に制約を設定します。制約は、子のルートビューのサイズと位置にのみ影響を与えます。子のビュー階層で、ルートビューや他のビューのコンテンツを変更しないでください。

子 View Controller を削除する

コンテンツから子 View Controller を削除するには、次の手順に従って View Controller 間の親子関係を削除します。

  1. nil を使用して、子の willMoveToParentViewController: メソッドを呼び出します。

  2. 子のルートビューを使用して設定した制約を削除します。

  3. 子のルートビューをコンテナのビュー階層から削除します。

  4. 子の removeFromParentViewController メソッドを呼び出して、親子関係の終了を最終処理します。

子 View Controller を削除すると、親子間の関係が永続的に切断されます。子 View Controller は、参照する必要がなくなったときにのみ削除してください。たとえば、新しい子 View Controller がナビゲーションスタックにプッシュされるとき、Navigation Controller はその現在の子 View Controller を削除しません。スタックから出されるときのみ削除します。

リスト 5-2」は、子 View Controller をそのコンテナから削除する方法を示しています。値 nil を使用して willMoveToParentViewController: メソッドを呼び出すと、変更の準備をするチャンスが子 View Controller に与えられます。removeFromParentViewController メソッドは、子の didMoveToParentViewController: メソッドも呼び出し、そのメソッドに値 nil を渡します。親 View Controller を nil に設定すると、コンテナからの子のビューの削除が最終処理されます。

リスト 5-2  子 View Controller をコンテナから削除する

- (void) hideContentController: (UIViewController*) content {
   [content willMoveToParentViewController:nil];
   [content.view removeFromSuperview];
   [content removeFromParentViewController];
}

子 View Controller 間で遷移する

別の子 View Controller への置き換えをアニメーション化する場合、子 View Controller の追加と削除をトランジションアニメーションプロセスに組み込みます。アニメーションの前に、両方の子 View Controller がコンテンツの一部となっていることを確認します。ただし、現在の子には、まもなく削除されることを伝えます。アニメーション中、新しい子のビューを所定の位置に移動し、古い子のビューを削除します。アニメーションの終了時、子 View Controller の削除を完了します。

リスト 5-3」は、トランジションアニメーションを使用して別の子 View Controller に入れ替える方法の例を示しています。この例では、現在既存の子 View Controller (画面から消えます) が表示されている四角形に新しい View Controller がアニメーション化されます。アニメーションが終了すると完了ブロックが子 View Controller をコンテナから削除します。この例では、transitionFromViewController:toViewController:duration:options:animations:completion: メソッドがコンテナのビュー階層を自動的に更新するため、ビュー自体を追加および削除する必要はありません。

リスト 5-3  2 つの子 View Controller 間で遷移する

- (void)cycleFromViewController: (UIViewController*) oldVC
               toViewController: (UIViewController*) newVC {
   // Prepare the two view controllers for the change.
   [oldVC willMoveToParentViewController:nil];
   [self addChildViewController:newVC];
 
   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.
   newVC.view.frame = [self newViewStartFrame];
   CGRect endFrame = [self oldViewEndFrame];
 
   // Queue up the transition animation.
   [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
            // Animate the views to their final positions.
            newVC.view.frame = oldVC.view.frame;
            oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
           // Remove the old view controller and send the final
           // notification to the new view controller.
           [oldVC removeFromParentViewController];
           [newVC didMoveToParentViewController:self];
        }];
}

子の外観の更新を管理する

子をコンテナに追加すると、コンテナは外観関連のメッセージを子に自動的に転送します。一般には、イベントがすべて適切に送信されるようになるので、これは望ましい動作と言えるでしょう。しかし場合によっては、コンテナの特性上、不適切な順序でイベントが配送されてしまうことがあります。たとえば、複数の子のビュー状態が同時に変化する場合、これを統合して、外観に関するコールバックが論理的に適切な順序で呼び出されるようにしたいことがあります。

外観コールバックの責任を引き継ぐには、「リスト 5-4」に示すように、Container View Controller で shouldAutomaticallyForwardAppearanceMethods メソッドをオーバーライドし、NO を返します。NO を返すと、Container View Controller がその外観の変更を子に通知することを UIKit が認識できます。

リスト 5-4  外観に関するコールバックの自動転送を無効にするコード例

- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
    return NO;
}

外観の遷移が発生したら、必要に応じて子の beginAppearanceTransition:animated: メソッドまたは endAppearanceTransition メソッドを呼び出します。たとえば、コンテナに子が 1 つあり、それが child プロパティとして参照されている場合、コンテナがメッセージを子に転送するコードは「リスト 5-5」のようになります。

リスト 5-5  コンテナの出現/消滅時に、外観に関するメッセージを転送するコード例

-(void) viewWillAppear:(BOOL)animated {
    [self.child beginAppearanceTransition: YES animated: animated];
}
 
-(void) viewDidAppear:(BOOL)animated {
    [self.child endAppearanceTransition];
}
 
-(void) viewWillDisappear:(BOOL)animated {
    [self.child beginAppearanceTransition: NO animated: animated];
}
 
-(void) viewDidDisappear:(BOOL)animated {
    [self.child endAppearanceTransition];
}

Container View Controller の構築に関するヒント

Container View Controller を新たに設計、開発、テストするには、相応の時間がかかります。個々の動作は単純でも、全体としてみれば非常に複雑なものになるからです。独自のコンテナクラスを実装する際には、次のヒントを検討してください。

子 View Controller へのコントロールをデリゲートする

Container View Controller は、自身の外観のいくつかの側面を 1 つ以上の子にデリゲートできます。コントロールは、次の方法でデリゲートできます。