Container View Controllerとは
一言で言うと、他のUIViewControllerを包含して表示するUIViewControllerのことです。どのように包含して表示するかによって、たとえばUINavigationControllerやUITabBarController、UIPageViewControllerのような実装があります。iOS 5からはこのContainer View Controllerを自作する事が可能になりましたが、実装が面倒なのと大体の場合においてUIKitが用意しているContainer View Controllerを使うかcocoapodsあたりからそれっぽいライブラリを拾ってくれば解決するためかあまり具体的な実装方法が話題になっていないようです。今回たまたま作る機会があったのでその時の内容をメモしておこうかと思います。
Container View ControllerへのView Controllerの追加
Container View Controllerを作るには、UIViewControllerを継承したクラスを作成して、そこに- (void)addViewController:(BOOL)animatedのような管理対象のView Controllerを追加するメソッドを作ってやればよいです。ここで、Container View ControllerにView Controllerを追加する上で基本的にやらなくてはならないことは以下の5ステップにわかれます。
- addChildViewController:
- このタイミングでContainer View Controllerに対象のView Controllerが格納されます。ただしViewはまだ表示されません。
- didMoveToParentViewController:
- Container View Controllerに対象のView Controllerが格納されたことを通知します。
- beginAppearanceTransition:animated:
- これからViewが表示されることをContainer View Controllerおよび対象のView Controllerに通知します。viewWillAppearに相当します。
- addSubview:
- 実際にViewを表示します。必要に応じてアニメーションもつけます。
- endAppearanceTransition
- Viewの表示が完了したことをContainer View Controllerおよび対象のView Controllerに通知します。viewDidAppearに相当します。
実際のサンプルコードはこちら。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- (void)addViewController:(BOOL)animated | |
{ | |
CGRect destFinalFrame = self.destinationFrame; | |
CGRect destInitialFrame = CGRectMake(CGRectGetMaxX(destFinalFrame), | |
CGRectGetMinY(destFinalFrame), | |
CGRectGetWidth(destFinalFrame), | |
CGRectGetHeight(destFinalFrame)); | |
UIViewController *srcViewController = self.sourceViewController; | |
UIViewController *destViewController = self.destinationViewController; | |
// 1. addChildViewController: | |
[srcViewController addChildViewController:destViewController]; | |
// 2. didMoveToParentViewController: | |
[destViewController didMoveToParentViewController:srcViewController]; | |
// 3. viewの設定 | |
destViewController.view.frame = destInitialFrame; | |
destViewController.view.autoresizingMask = UIViewAutoresizingNone; | |
// 4. beginAppearanceTransition:animated: | |
[srcViewController beginAppearanceTransition:NO animated:animated]; | |
[destViewController beginAppearanceTransition:YES animated:animated]; | |
// 5. addSubview: | |
[srcViewController.view addSubview:destViewController.view]; | |
// 6. viewのアニメーション | |
void (^before)() = ^{ | |
// any pre-animation process | |
}; | |
void (^block)() = ^{ | |
// animation block | |
destViewController.view.frame = destFinalFrame; | |
}; | |
void (^finish)() = ^{ | |
// 7. endAppearanceTransition | |
[srcViewController endAppearanceTransition]; | |
[destViewController endAppearanceTransition]; | |
}; | |
if (animated) { | |
before(); | |
[[UIApplication sharedApplication] beginIgnoringInteractionEvents]; | |
[UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:block completion:^(BOOL finished) { | |
[[UIApplication sharedApplication] endIgnoringInteractionEvents]; | |
finish(); | |
}]; | |
} else { | |
before(); | |
block(); | |
finish(); | |
} | |
} |
Container View ControllerからのView Controllerの削除
View Controllerに追加した時に実施したことを逆順に実施してやればOKです。- (void)removeViewController:(BOOL)animatedのようなメソッドを作ってやって、そこに実装を書けばよいでしょう。サンプルコードにするとこんな感じになります。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
- (void)removeViewController:(BOOL)animated | |
{ | |
CGRect destInitialFrame = self.destinationFrame; | |
CGRect destFinalFrame = CGRectMake(CGRectGetMaxX(destInitialFrame), | |
CGRectGetMinY(destInitialFrame), | |
CGRectGetWidth(destInitialFrame), | |
CGRectGetHeight(destInitialFrame)); | |
UIViewController *srcViewController = self.sourceViewController; | |
UIViewController *destViewController = self.destinationViewController; | |
// 1. beginAppearanceTransition:animated: | |
[srcViewController beginAppearanceTransition:YES animated:animated]; | |
[destViewController beginAppearanceTransition:NO animated:animated]; | |
// 2. viewのアニメーション | |
void (^before)() = ^{ | |
// any pre-animation process | |
}; | |
void (^block)() = ^{ | |
// animation block | |
destViewController.view.frame = destFinalFrame; | |
}; | |
void (^finish)() = ^{ | |
// 3. removeFromSuperview | |
[destViewController.view removeFromSuperview]; | |
// 4. endAppearanceTransition | |
[destViewController endAppearanceTransition]; | |
[srcViewController endAppearanceTransition]; | |
// 5. willMoveToParentViewController: | |
[destViewController willMoveToParentViewController:nil]; | |
// 6. removeFromParentViewController | |
[destViewController removeFromParentViewController]; | |
}; | |
if (animated) { | |
before(); | |
[[UIApplication sharedApplication] beginIgnoringInteractionEvents]; | |
[UIView animateWithDuration:self.animationDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:block completion:^(BOOL finished) { | |
[[UIApplication sharedApplication] endIgnoringInteractionEvents]; | |
finish(); | |
}]; | |
} else { | |
before(); | |
block(); | |
finish(); | |
} | |
} |
shouldAutomaticallyForwardAppearanceMethodsの設定
先に答えだけいうと、何もしないでいいです。このメソッドの存在そのものを忘れて構いません。iOS 6から追加されたメソッドで、overrideして使用します。このメソッドがYESを返すときは、Container View Controller自身が他のContainer View Controllerに追加されるなどして表示されviewWillAppear/viewDidAppearが呼び出されるタイミングで自動的にchildViewControllersに対してもviewWillAppear/viewDidAppearを呼び出します。NOの場合は自動的に呼び出されないため手動でchildViewControllersの表示状態を管理し、適時beginAppearanceTransition:animated:を呼び出す必要があります。
デフォルトはYESで、基本的にはデフォルトのまま使えば問題ありません。NOを返したいケースは、たとえばContainer View Controllerが画面に表示されてから一瞬遅れてchildViewControllersをアニメーションしながら表示したいなどの要件がある場合に限られるでしょう。