KittenYang

iOS自定义转场详解01——UIViewControllerTransitioning的用法

本文是我学习了onevcat的这篇转场入门的一点笔记。

老规矩,我不打算先讲理论再给例子。我们上来就直接拿活的练。

今天实现一个简单的自定义转场:

首页,用Storyboard快速创建两个ViewController。一个作为住主控制器,叫ViewController ; 另一个作为是转过去的副控制器,叫PresentedViewController。并且用Autolayout快速搭建好界面。就像这样:

由于我使用了Segue,所以可以只需要一句话:

//ViewController.m
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

    PresentedViewController *pvc = segue.destinationViewController;
    pvc.delegate = self;

}

就完成了最最基本的转场。like this:

然后实现点击关闭的功能。这里要说明一下,很多朋友喜欢在-buttonClicked:中直接给self发送dismissViewController的相关方法。在现在的SDK中,如果当前的VC是被显示的话,这个消息会被直接转发到显示它的VC去。但是这并不是一个好的实现,违反了程序设计的哲学,也很容易掉到坑里。所以我们用标准的delegate 方式实现 dismiss

首先在PresentedViewController 控制器中声明一个代理。

//PresentedViewController.h
@class PresentedViewController;
@protocol PresentedVCDelegate <NSObject>

-(void)didPresentedVC:(PresentedViewController *)viewcontroller;

@end

@interface PresentedViewController : UIViewController
@property(nonatomic,weak) id<PresentedVCDelegate>delegate;

@end

在button的点击事件中,让delegate去完成关闭当前VC的工作。

//PresentedViewController.m
- (IBAction)dismissClicked:(id)sender {
    if (self.delegate && [self.delegate respondsToSelector:@selector(didPresentedVC:)]) {
        [self.delegate didPresentedVC:self];
    }
}

与此同时,ViewController中需要 pvc.delegate = self; 。然后实现这个代理方法。

-(void)didPresentedVC:(PresentedViewController *)viewcontroller{
    [self dismissViewControllerAnimated:YES completion:nil];
}

运行,一个最简单的专场就实现了。

接下来,我们用iOS7中一个新的类 UIViewControllerTransitioning 来实现自定义转场。


第一步:UIViewControllerAnimatedTransitioning

首先,我们需要一个实现了协议名为 UIViewControllerAnimatedTransitioning 的对象。创建一个类叫做 RotationPresentAnimation 继承于 NSObject 并实现了协议 UIViewControllerAnimatedTransitioning

@interface RotationPresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>

这个协议负责切换的具体内容,也即“切换中应该发生什么”。开发者在做自定义切换效果时大部分代码会是用来实现这个接口。 这个协议只有两个方法:

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext; //返回动画的时间
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext; //在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。

实现这两个方法:

- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
    return 0.5f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{
    //1
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    //2
    CGRect finalRect = [transitionContext finalFrameForViewController:toVC];
    toVC.view.frame = CGRectOffset(finalRect, 0, [[UIScreen mainScreen]bounds].size.height);

    //3
    [[transitionContext containerView]addSubview:toVC.view];

    //4
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
        toVC.view.frame = finalRect;
    } completion:^(BOOL finished) {
        //5
        [transitionContext completeTransition:YES];
    }];
}

解释一下:

1、我们需要得到参与切换的两个ViewController的信息,使用context的方法拿到它们的参照;
2、对于要呈现的VC,我们希望它从屏幕下方出现,因此将初始位置设置到屏幕下边缘;
3、将view添加到containerView中;
4、开始动画。这里的动画时间长度和切换时间长度一致。usingSpringWithDamping的UIView动画API是iOS7新加入的,描述了一个模拟弹簧动作的动画曲线;
5、在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。

第二步:UIViewControllerTransitioningDelegate

这个接口的作用比较单一,在需要VC切换的时候系统会向实现了这个接口的对象询问是否需要使用自定义的切换效果。

所以,一个比较好的地方是直接在主控制器 ViewController 中实现这个协议。

ViewController.m 中完成如下代码:

@interface ViewController ()<PresentedVCDelegate,UIViewControllerTransitioningDelegate>

@property(nonatomic,strong)RotationPresentAnimation *presentAnimation;

@end

...

//1
-(instancetype)initWithCoder:(NSCoder *)aDecoder{
    self = [super initWithCoder:aDecoder];
    if (self) {
        self.presentAnimation = [[RotationPresentAnimation alloc]init];
    }
    return self;
}

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{

    ...
    //2
    pvc.transitioningDelegate = self;
    ...

}

//3
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
    return self.presentAnimation;

}

解释: 1、由于我们用Storyboard创建的界面,所以初始化应该用-(instancetype)initWithCoder:(NSCoder *)aDecoder 方法。顺便补充,如果用的是代码或者xib创建的界面,则应该使用
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil 初始化方法;

2、副控制器的代理设置成主控制器;

3、主控制器实现代理方法。这个方法,我们只需要在呈现VC的时候,给出一个实现了 UIViewControllerAnimatedTransitioning 协议的对象。这里,显然就是继承于RotationPresentAnimationpresentAnimation

运行,看一下。


*进阶——实现手势驱动的百分比切换

现在我们增加一个功能,就是用手势滑动来dismiss。通俗的说,就是让新加的那个VC手势取消,而且很跟手。

1.首先新建一个类,继承自UIPercentDrivenInteractiveTransition .

//PanInteractiveTransition.h
@interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition

-(void)panToDismiss:(UIViewController *)viewcontroller;
@end

1)我们写一个方法提供给外部类调用。让外部类可以看到传入手势dismiss的VC的入口。

2.既然传入了这个需要手势dismiss的VC,我们就需要保存一下,方便在当前类的其他地方使用。所以我们新建一个属性来保存这个传入的VC。

//PanInteractiveTransition.m
@interface PanInteractiveTransition()
@property(nonatomic,strong)UIViewController *presentedVC;

@end

-(void)panToDismiss:(UIViewController *)viewcontroller{
    self.presentedVC = viewcontroller;
    UIPanGestureRecognizer *panGstR = [[UIPanGestureRecognizer alloc]initWithTarget:self action:@selector(panGestureAction:)];
    [self.presentedVC.view addGestureRecognizer:panGstR];

}

#pragma mark -panGestureAction
-(void)panGestureAction:(UIPanGestureRecognizer *)gesture{
    CGPoint translation = [gesture translationInView:self.presentedVC.view];
    switch (gesture.state) {
        case UIGestureRecognizerStateChanged:{
            //1
            CGFloat percent = (translation.y/300) <= 1 ? (translation.y/300):1;
            [self updateInteractiveTransition:percent];
            break;
        }
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateEnded:{
            //2
            if (gesture.state == UIGestureRecognizerStateCancelled) {
                [self cancelInteractiveTransition];
            }else{
                [self finishInteractiveTransition];
            }
            break;
        }

        default:
            break;
    }
}

1)我们把滑动400px作为临界值。让滑动的距离除以400获得一个百分比系数,当这个系数大于1的时候取1。
2)UIGestureRecognizerStateCancelled 的时候 [self cancelInteractiveTransition]; ; UIGestureRecognizerStateEnded 的时候 [self finishInteractiveTransition]; .

3.和 RotationPresentAnimation 一样,我们需要创建一个 RotationDismissAnimation

//RotationDismissAnimation.h
@interface RotationDismissAnimation : NSObject<UIViewControllerAnimatedTransitioning>

@end

//RotationDismissAnimation.m
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext{
    return 1.0f;
}

- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext{

    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    CGRect initRect  = [transitionContext initialFrameForViewController:fromVC];
    CGRect finalRect = CGRectOffset(initRect, 0, [[UIScreen mainScreen]bounds].size.height);

    UIView *containerView = [transitionContext containerView];
    [containerView addSubview:toVC.view];
    [containerView sendSubviewToBack:toVC.view];

    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0f usingSpringWithDamping:0.4f initialSpringVelocity:0.0f options:UIViewAnimationOptionCurveLinear animations:^{
        fromVC.view.frame = finalRect;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }];
}

4.最后,我们需要在主控制器中 ViewController.m 中添加收尾的三步。

//1
@property(nonatomic,strong)RotationDismissAnimation *dismissAnimation;
@property(nonatomic,strong)PanInteractiveTransition *panInteractiveTransition;
//2
[self.panInteractiveTransition panToDismiss:pvc];
//3
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return self.dismissAnimation;
}


- (id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator{
    return self.panInteractiveTransition;
}

运行看看,现在已经可以手势百分比驱动了。

完善

仔细的你一定会发现,以上代码尽管实现了手势驱动,但是点击按钮dismiss的功能无法使用了。这是因为,如果只是返回self.panInteractiveTransition, 那么点击按钮dismiss的动画将无法使用;如果只是返回nil, 那么手势滑动的效果将会无法使用。

综上,我们应该分情况分别返回。

剩下的就是一些细节问题了。比如上面提到的分情况返回手势驱动还是点击按钮、超过多少距离自动dismiss、未超过多少距离复原。我完成了余下的工作,完整代码你可以在这里 获取。

总结

对与Navigation Controller的Push和Pop切换也是有相应的一套方法的。实现起来和dismiss十分类似,只不过对应UIViewControllerTransitioningDelegate的询问动画和交互的方法换到了UINavigationControllerDelegate中(为了区别push或者pop,看一下这个接口应该能马上知道)。

需要特别一提的是,Github上的ColinEberhardt的VCTransitionsLibrary已经为我们提供了一系列的VC自定义切换动画效果。在这上面修修改改能获得更炫的转场动画,一起来创造吧!

KittenYang

写写代码,做做设计,看看产品。