[iOS] ナビゲーションコントローラのトランジションを手軽に差し替える方法

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

UINavigationControllerのトランジションエフェクトを簡単に変えたい

業務でiOSアプリ開発をやっていると、時々画面のトランジションエフェクトを全体的にデフォルト以外のものに変えてほしいという要望を頂く事があります。トランジションエフェクトの要望が局所的な場合や画面数があまり多くない場合は、個別に実装してしまっても問題ありません。しかし、画面数がそこそこあるアプリだと、UINavigationControllerで画面遷移を管理しつつ、デフォルトのPushトランジションエフェクトを置き換えてしまった方が楽です。そこで、今回はUINavigationControllerのトランジションエフェクトをデフォルトのPush以外に簡単に置き換える方法をご紹介します。

開発環境

今回の開発環境は下記の通りです。

  • OSX 10.8
  • Xcode 4.6
  • iOS SDK 6.1

UINavigationControllerで画面遷移を作る

とりあえず、UINavigationControllerで画面遷移を作ってしまいます。Storyboardでさくっと作りました。

ios-navtransition001

オレンジ色のビューと青色のビューをPushSegueでつないでいます。オレンジ色のビューのNextボタンを押すと青色のビューに遷移し、青色のビューのBackボタンを押すとオレンジ色のビューに戻ります。当然ですが、この状態で実行すると遷移の際にPushトランジションエフェクトが適用されています。

ios-navtransition002

UINavigationControllerのトランジションエフェクトを置き換える

さて、デフォルトのPushトランジションエフェクトを置き換える方法ですが、やり方はいたってシンプルです。UINavigationControllerのサブクラスを作成して、pushViewController:animatedやpopViewControllerAnimated:などのメソッドをオーバーライドするだけです。トランジションエフェクトの適用方法は2種類あります。

UIViewのトランジションメソッドを利用する

push時のトランジションエフェクトを置き換える

まず一つ目の方法は、UIViewのトランジション用クラスメソッドを利用する方法です。下記コードでは、UINavigationControllerのサブクラスでpushViewController:animated:をオーバーライドしています。

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (animated) {
        [UIView transitionWithView:self.view duration:0.6f options:UIViewAnimationOptionTransitionCurlUp animations:^{
            [super pushViewController:viewController animated:NO];
        } completion:nil];
    } else {
        [super pushViewController:viewController animated:NO];
    }
}

UIViewのクラスメソッドであるtransitionWithView:duration:options:animations:completion:を利用してトランジションエフェクトを適用しています。このメソッドは最初のパラメータで渡されたViewのsubViewに対して、ビュー階層に追加・削除される際にトランジションエフェクトを再生してくれます。

トランジションエフェクトの適用対象となるビューの追加・削除処理は、animationsパラメータで渡すblocksの中で行います。UINavigationControllerのpushViewController:animated:メソッドの中でビューのaddSubViewやremoveFromSuperViewなどの操作が行われているので、animationsパラメータで渡しているblocksの中でスーパークラスの実装を呼び出しています。

また、アニメーションはUIViewのトランジションメソッドで行うので、animatedパラメータにはNOを渡しておきます。

optionsパラメータはUIViewAnimationOptions型で、トランジション時の様々なオプションを指定するために利用します。今回は、トランジションエフェクトにCurlUpを指定しています。このパラメータはビットフラグになっているので、イージングカーブなど複数のオプションを指定したい場合にはOR演算で渡す事ができます。以下は、UIVIewAnimationOptions型の定義です。トランジションエフェクトは7種類から選択できます。

typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    // 諸々の設定
    UIViewAnimationOptionLayoutSubviews            = 1 <<  0,
    UIViewAnimationOptionAllowUserInteraction      = 1 <<  1, // turn on user interaction while animating
    UIViewAnimationOptionBeginFromCurrentState     = 1 <<  2, // start all views from current value, not initial value
    UIViewAnimationOptionRepeat                    = 1 <<  3, // repeat animation indefinitely
    UIViewAnimationOptionAutoreverse               = 1 <<  4, // if repeat, run animation back and forth
    UIViewAnimationOptionOverrideInheritedDuration = 1 <<  5, // ignore nested duration
    UIViewAnimationOptionOverrideInheritedCurve    = 1 <<  6, // ignore nested curve
    UIViewAnimationOptionAllowAnimatedContent      = 1 <<  7, // animate contents (applies to transitions only)
    UIViewAnimationOptionShowHideTransitionViews   = 1 <<  8, // flip to/from hidden state instead of adding/removing
    
    // イージングカーブの設定
    UIViewAnimationOptionCurveEaseInOut            = 0 << 16, 
    UIViewAnimationOptionCurveEaseIn               = 1 << 16,
    UIViewAnimationOptionCurveEaseOut              = 2 << 16,
    UIViewAnimationOptionCurveLinear               = 3 << 16,
    
    // トランジションエフェクトの設定
    UIViewAnimationOptionTransitionNone            = 0 << 20, // トランジションエフェクトなし
    UIViewAnimationOptionTransitionFlipFromLeft    = 1 << 20, // 左からFlip
    UIViewAnimationOptionTransitionFlipFromRight   = 2 << 20, // 右からFlip
    UIViewAnimationOptionTransitionCurlUp          = 3 << 20, // CurlUp
    UIViewAnimationOptionTransitionCurlDown        = 4 << 20, // CurlDown
    UIViewAnimationOptionTransitionCrossDissolve   = 5 << 20, // CrossDissolve
    UIViewAnimationOptionTransitionFlipFromTop     = 6 << 20, // 上からFlip
    UIViewAnimationOptionTransitionFlipFromBottom  = 7 << 20, // 下からFlip
} NS_ENUM_AVAILABLE_IOS(4_0);
[/c]</p>

<h3>pop時のトランジションを置き換える</h3>
<p>popViewControllerAnimated:などのpopのための3つのメソッドも同じ様に実装します。下記コードはUINavigationControllerサブクラスの実装ファイルです。</p>
<p>
#import "UIViewTransitionNavigationController.h"

@interface UIViewTransitionNavigationController ()

@end

@implementation UIViewTransitionNavigationController

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (animated) {
        [UIView transitionWithView:self.view duration:0.6f options:UIViewAnimationOptionTransitionCurlUp animations:^{
            [super pushViewController:viewController animated:NO];
        } completion:nil];
    } else {
        [super pushViewController:viewController animated:NO];
    }
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    __block NSArray *viewControllers = nil;
    
    if (animated) {
        [self popWithTransitionAnimations:^{
            viewControllers = [super popToViewController:viewController animated:NO];
        }];
    } else {
        viewControllers = [super popToViewController:viewController animated:NO];
    }
    
    return viewControllers;
}

- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    __block UIViewController *popedViewController = nil;
    
    if (animated) {
        [self popWithTransitionAnimations:^{
            popedViewController = [super popViewControllerAnimated:NO];
        }];
    } else {
        popedViewController = [super popViewControllerAnimated:NO];
    }
    
    return popedViewController;
}

- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
{
    __block NSArray *viewControllers = nil;
    
    if (animated) {
        [self popWithTransitionAnimations:^{
            viewControllers = [super popToRootViewControllerAnimated:NO];
        }];
    } else {
        viewControllers = [super popToRootViewControllerAnimated:NO];
    }
    
    return viewControllers;
}

- (void)popWithTransitionAnimations:(void (^)(void))animations
{
    [UIView transitionWithView:self.view
                      duration:0.6f
                       options:UIViewAnimationOptionTransitionCurlDown
                    animations:animations
                    completion:nil];
}

@end

NavigationControllerを差し替える

あとは、作成したカスタムNavigationControllerを標準のUINavigationControllerと差し替えるだけで画面遷移時のトランジションエフェクトを置き換えることができます。Storyboard上でNavigationControllerのクラスとして、先ほど作成したクラスを指定します。コードでUINavigationControllerを生成する場合は、単にカスタムNavigationControllerをインスタンス化してUINavigationControllerの代わりに使用すればOKです。

ios-navtransition003

出来上がり

これでNavigationControllerを利用した画面遷移時のトランジションエフェクトがCurlになりました。以下は実行時の様子です。

ios-navtransition006

CATransitionを利用する

CATransitionはCore Animationで用意されているクラスで、CAAnimationのトランジション専用のサブクラスです。2つ目の方法ではこちらを利用してトランジションエフェクトを適用します。プロジェクトにQuartzCore.frameworkを追加して下さい。

push時のトランジションエフェクトを置き換える

下記コードでは、UINavigationControllerのサブクラスでpushViewController:animated:をオーバーライドしています。

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [self.view.layer removeAllAnimations];
    
    if (animated) {
        CATransition *transition = [self pushTransition];
        [self.view.layer addAnimation:transition forKey:kCATransition];
    }
    
    [super pushViewController:viewController animated:NO];
}

- (CATransition *)pushTransition
{
    CATransition *transition = [CATransition animation];
    transition.duration = 0.6f;
    transition.type = kCATransitionReveal;
    transition.subtype = kCATransitionFromRight;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    
    return transition;
}

CATransitionのインスタンスを作成して、NavigationControllerのviewのルートレイヤーにアニメーションとして追加しています。CATransitionのtypeプロパティでトランジションエフェクトの種類を、subtypeでトランジションエフェクトの方向を指定しています。トランジションエフェクトは以下のものが指定できます。

  • Fade(kCATransitionFade)
  • MoveIn(kCATransitionMoveIn)
  • Push(kCATransitionPush)
  • Reveal(kCATransitionReveal)

今回はRevealを使いました。なお、typeプロパティはNSString型で、上記の定数以外にもpageCurlなどが指定できます。ただし、ヘッダに定数が切られておらずドキュメントにも記載されていないので、上記の定数以外を利用するとAppStore申請時にリジェクトされる可能性が高いです。

pop時のトランジションエフェクトを置き換える

viewControllerをpopする3つのメソッドを同じ様に実装します。下記コードはUINavigationControllerサブクラスの実装ファイルです。

#import "CATransitionNavigationController.h"
#import <QuartzCore/QuartzCore.h>

@interface CATransitionNavigationController ()

@end

@implementation CATransitionNavigationController

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [self.view.layer removeAllAnimations];
    
    if (animated) {
        CATransition *transition = [self pushTransition];
        [self.view.layer addAnimation:transition forKey:kCATransition];
    }
    
    [super pushViewController:viewController animated:NO];
}

- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    [self.view.layer removeAllAnimations];
    
    if (animated) {
        CATransition *transition = [self popTransition];
        [self.view.layer addAnimation:transition forKey:kCATransition];
    }
    
    return [super popToViewController:viewController animated:NO];
}

- (UIViewController *)popViewControllerAnimated:(BOOL)animated
{
    [self.view.layer removeAllAnimations];
    
    if (animated) {
        CATransition *transition = [self popTransition];
        [self.view.layer addAnimation:transition forKey:kCATransition];
    }
    
    return [super popViewControllerAnimated:NO];
}

- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
{
    [self.view.layer removeAllAnimations];
    
    if (animated) {
        CATransition *transition = [self popTransition];
        [self.view.layer addAnimation:transition forKey:kCATransition];
    }
    
    return [super popToRootViewControllerAnimated:NO];
}

- (CATransition *)pushTransition
{
    CATransition *transition = [CATransition animation];
    transition.duration = 0.6f;
    transition.type = kCATransitionReveal;
    transition.subtype = kCATransitionFromRight;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    
    return transition;
}

- (CATransition *)popTransition
{
    CATransition *transition = [CATransition animation];
    transition.duration = 0.6f;
    transition.type = kCATransitionReveal;
    transition.subtype = kCATransitionFromLeft;
    transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
    
    return transition;
}

@end

NavigationControllerを差し替える

あとは、先程と同じ様にNavigationControllerのクラスを作成したものに差し替えれば作業は完了です。

出来上がり

これでNavigationControllerを利用した画面遷移時のトランジションエフェクトがRevealになりました。以下は実行時の様子です。

ios-navtransition007

この方法を利用するにあたっての注意点

この方法でトランジションエフェクトを置き換えたNavigationControllerを利用してナビゲーションバーやツールバーを表示している場合、それらもトランジションエフェクトの対象となってしまいます。

ios-navtransition008

これは、NavigationControllerのview(のルートレイヤー)に対してトランジションのアニメーションを適用しているためです。UINavigationControllerは内部に複数のUIViewを持っており、コンテンツ部分はUINavigationTransitionViewというプライベートクラスをコンテナにして管理されています。通常、トランジションはこのビューとそのサブビューに対して適用されています。以下は、ViewControllerを一つ追加したUINavigationControllerのview階層をダンプした結果です。

<UILayoutContainerView: 0x7579200; frame = (0 0; 320 568); autoresize = W+H; animations = { transition=<CATransition: 0x9167db0>; }; layer = <CALayer: 0x75792c0>>
   | <UINavigationTransitionView: 0x7579810; frame = (0 0; 320 568); clipsToBounds = YES; autoresize = W+H; layer = <CALayer: 0x75798e0>>
   |    | <UIViewControllerWrapperView: 0x757b1d0; frame = (0 20; 320 548); autoresize = RM+BM; layer = <CALayer: 0x757fb90>>
   |    |    | <UIView: 0x757f940; frame = (0 0; 320 548); autoresize = RM+BM; layer = <CALayer: 0x757fa30>>
   |    |    |    | <UIRoundedRectButton: 0x757c420; frame = (123.5 485; 73 44); opaque = NO; autoresize = TM+BM; layer = <CALayer: 0x757c540>>
   |    |    |    |    | <UIGroupTableViewCellBackground: 0x757cd30; frame = (0 0; 73 44); userInteractionEnabled = NO; layer = <CALayer: 0x757ce00>>
   |    |    |    |    | <UIImageView: 0x757e470; frame = (1 1; 71 43); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x757ea30>>
   |    |    |    |    | <UIButtonLabel: 0x757dc40; frame = (12 12; 49 19); text = 'Button'; clipsToBounds = YES; opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x757dd30>>
   | <UINavigationBar: 0x7575b30; frame = (0 -24; 320 44); hidden = YES; autoresize = W; gestureRecognizers = <NSArray: 0x7578040>; layer = <CALayer: 0x7575c00>>
   |    | <_UINavigationBarBackground: 0x7577120; frame = (0 0; 320 44); opaque = NO; autoresize = W; userInteractionEnabled = NO; layer = <CALayer: 0x75771c0>>
   |    |    | <UIImageView: 0x7577770; frame = (0 44; 320 3); opaque = NO; autoresize = W+TM; userInteractionEnabled = NO; layer = <CALayer: 0x75777d0>>
   |    | <UINavigationItemView: 0x75760d0; frame = (160 22; 0 0); opaque = NO; userInteractionEnabled = NO; layer = <CALayer: 0x75761c0>>

このため、ナビゲーションバーやツールバーを表示していてなおかつどうしてもトランジションの対象にしたくないという場合は、下記のようにして実現できますが、少々ダーティな方法のためあまりおすすめしません。

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (animated) {
        UIView *targetView = (UIView *)self.view.subviews[0]; // UINavigationTransitionView
        [UIView transitionWithView:targetView duration:0.6f options:UIViewAnimationOptionTransitionCurlUp animations:^{
            [super pushViewController:viewController animated:NO];
        } completion:nil];
    } else {
        [super pushViewController:viewController animated:NO];
    }
}

実際にトランジションエフェクトを変えたいという要望が出る場合には、標準のナビゲーションバーなどは表示していないケースがほとんどだとは思いますが、一応注意が必要です。