2014年12月26日金曜日

Container View Controllerを作ってみよう

今日は冬休みの工作ということで、iOSのContainer View Controllerを作ってみようと思います。

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ステップにわかれます。
  1. addChildViewController:
    • このタイミングでContainer View Controllerに対象のView Controllerが格納されます。ただしViewはまだ表示されません。
  2. didMoveToParentViewController:
    • Container View Controllerに対象のView Controllerが格納されたことを通知します。
  3. beginAppearanceTransition:animated:
    • これからViewが表示されることをContainer View Controllerおよび対象のView Controllerに通知します。viewWillAppearに相当します。
  4. addSubview:
    • 実際にViewを表示します。必要に応じてアニメーションもつけます。
  5. endAppearanceTransition
    • Viewの表示が完了したことをContainer View Controllerおよび対象のView Controllerに通知します。viewDidAppearに相当します。

実際のサンプルコードはこちら。
- (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のようなメソッドを作ってやって、そこに実装を書けばよいでしょう。
サンプルコードにするとこんな感じになります。
- (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をアニメーションしながら表示したいなどの要件がある場合に限られるでしょう。