It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【iOS】画面遷移にUIKit Dynamicsのアニメーションを使ってみる

iOS7から使えるようになったカスタムの画面遷移UIViewControllerTransitioningDelegateと 同じくiOS7から使えるようになった物理エンジンのラッパーUIKit Dynamicsを 組み合わせて画面遷移アニメーションを作ってみたいと思います。

今回は、重力によって地面に落ちて画面遷移するアニメーションを作成します。
落ちるときは、画面下に衝突した時に跳ねるようにしています。
また、表示するときは、天井にぶつかって跳ねる用にしました。

UIKit Dynamicsの準備

まずViewControllerの画面遷移で使用するUIViewControllerAnimatedTransitioningに準拠したクラスを作成します。
今回は、UIKit Dynamicsを使ってアニメーションを行いたいので、UIDynamicBehaviorクラスを継承して作成します。

■ DropTransition.h

@interface INNDropViewBehavior : UIDynamicBehavior
<UIViewControllerAnimatedTransitioning>

@end

■ DropTransition.m

// class extension
@interface DropTransition ()
<UIViewControllerAnimatedTransitioning, UIDynamicAnimatorDelegate>

@property (nonatomic, strong) UIDynamicAnimator *animator;
@property (nonatomic, strong) id <UIViewControllerContextTransitioning> transitionContext;

@end

@implementation DropTransition

#pragma mark UIViewControllerAnimatedTransitioning
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    self.transitionContext = transitionContext;
    
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC   = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    
    UIView *containerView = [transitionContext containerView];
    
    // アニメーション対象のView(=遷移先のView)
    UIView *frontView = nil;
    UIView *backView  = nil;
    CGVector gravityDirection;
    
    if (self.isPresent) { // 表示
        
        frontView        = toVC.view;
        backView         = fromVC.view;
        // 画面の表示と非表示で重力の方向を逆にする
        gravityDirection = CGVectorMake(0, -1.0);
        frontView.frame  = CGRectOffset(frontView.frame, 0, frontView.bounds.size.height);
        
    } else { // 画面閉じる
        
        frontView = fromVC.view;
        backView  = toVC.view;
        gravityDirection = CGVectorMake(0, 1.0);
    }
    
    /* Viewの準備 */
    [containerView addSubview:backView];
    
    // アニメーションを行うViewは、跳ね返りをするために縦方向に2倍の高さを取る
    CGRect frame = [transitionContext initialFrameForViewController:fromVC];
    UIView *canvasView = [[UIView alloc] initWithFrame:CGRectMake(0, 0,
                                                                   frame.size.width,
                                                                   frame.size.height * 2)];
    
    [canvasView addSubview:frontView];
    [containerView addSubview:canvasView];
    
    
    /* UIKitDynamicsの準備 */
    self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:canvasView];
    self.animator.delegate = self;
    
    // 重力
    UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:@[frontView]];
    [self addChildBehavior:gravityBehavior];
    gravityBehavior.gravityDirection = gravityDirection;
    
    // 衝突
    UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:@[frontView]];
    collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
    [self addChildBehavior:collisionBehavior];
    
    // property
    UIDynamicItemBehavior *propertyBehavior = [[UIDynamicItemBehavior alloc] initWithItems:@[frontView]];
    propertyBehavior.elasticity             = 0.4; // 弾力
    propertyBehavior.friction               = 1.0; // 摩擦
    [self addChildBehavior:propertyBehavior];

    
    [self.animator addBehavior:self];
    
}

// TODO: 物理計算のアニメーションなので、秒数が正確にとれない..
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
    return 0;
}

#pragma mark - UIViewControllerAnimatedTransitioning
- (void)animationEnded:(BOOL)transitionCompleted
{
    // contextの解放
    [self.animator.referenceView removeFromSuperview];
    self.animator = nil;
    self.transitionContext = nil;
}

#pragma mark -
#pragma mark UIDynamicAnimatorDelegate
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator
{
    // アニメーションが終わった時点で通知する
    [self.transitionContext completeTransition:YES];
}

@end

ViewControllerの準備

次に、画面遷移元となるViewControllerにUIViewControllerTransitioningDelegateのデリゲートメソッドを準備します。

■ ViewController.m

// 遷移は、segueを使用
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"moveto"]) {
         // 遷移先のViewControllerのtransitionを独自で行うようにdelegateを指定する
        [segue.destinationViewController setTransitioningDelegate:self];
    }
}

#pragma mark - UIViewControllerAnimatedTransitioning
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    DropTransition *transition = [[DropTransition alloc] init];
    transition = NO;
    
    return behavior;
}

- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    DropTransition *behavior = [[DropTransition alloc] init];
    behavior.isPresent = YES;
    
    return transition;

}

これだけで、モーダルの遷移を独自に実装できます。
面白いアニメーションも簡単に実装できそうなので色々試してみたいです。


画面の用途によってアニメーションを切り分けたい場合は、UIViewControllerAnimatedTransitioningは遷移先のViewControllerに設定するでもいいかもしれません。