iOS开发中,页面之间的跳转无外乎由UINavigationController管理的push或者pop操作、以及由UIViewController管理的present和dismiss操作,无论何种操作,iOS原生系统都为我们提供了页面之间的基础跳转动画。但是往往在开发中,由于各种功能需求,iOS原生系统提供的跳转动画并不能满足我们的需求,好在iOS早就给我们提供了一套自定义转场动画的解决方案,这篇文章就来详细了解一下转场动画。在了解这篇文章之前,先看看iOS提供的整个转场框架
 
present/dismiss 首先,我们先来了解一下模态跳转。开发中,假如在A界面需要模态跳转到B界面,通常会这么写:- (void )touchesBegan:(NSSet <UITouch  *> *)touches withEvent:(UIEvent  *)event {
    PresentBViewController *bVC = [[PresentBViewController alloc] init];
    [self  presentViewController:bVC animated:YES  completion:nil ];
}
模态消失当前界面则是这么写:- (void )touchesBegan:(NSSet <UITouch  *> *)touches withEvent:(UIEvent  *)event {
    [self  dismissViewControllerAnimated:YES  completion:nil ];
}
UIViewControllerTransitioningDelegate 使用模态跳转时,系统已经为我们写好的跳转的动画,而要想自定义模态跳转动画,则需要一个实现UIViewControllerTransitioningDelegate协议、并实现协议部分方法的对象。在UIViewController.h头文件中,我们可以发现如下定义:@protocol  UIViewControllerTransitioningDelegate ;
@interface  UIViewController (UIViewControllerTransitioning )
@property  (nullable , nonatomic , weak ) id  <UIViewControllerTransitioningDelegate > transitioningDelegate NS_AVAILABLE_IOS (7 _0);
@end 
如此,我们则需要定义一个对象,并实现UIViewControllerTransitioningDelegate协议方法,赋值给将要模态跳转的控制器(PresentBViewController),如下:@interface  PresentAViewController  ()
@property  (nonatomic , strong ) PresentManager *presentManager;
@end 
@implementation  PresentAViewController 
- (void )viewDidLoad {
    [super  viewDidLoad];
    self .view.backgroundColor = [UIColor  blueColor];
}
- (void )touchesBegan:(NSSet <UITouch  *> *)touches withEvent:(UIEvent  *)event {
    PresentBViewController *bVC = [[PresentBViewController alloc] init];
    self .presentManager = [[PresentManager alloc] init];
    bVC.transitioningDelegate = self .presentManager;
    [self  presentViewController:bVC animated:YES  completion:nil ];
}
@end 
我们先来看看UIViewControllerTransitioningDelegate有哪些协议方法:
- (nullable  id  <UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController  *)presented
                                                                            presentingController:(UIViewController  *)presenting
                                                                                sourceController:(UIViewController  *)source;
- (nullable  id  <UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController  *)dismissed;
- (nullable  id  <UIViewControllerInteractiveTransitioning >)interactionControllerForPresentation:(id  <UIViewControllerAnimatedTransitioning >)animator;
- (nullable  id  <UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id  <UIViewControllerAnimatedTransitioning >)animator;
- (nullable  UIPresentationController  *)presentationControllerForPresentedViewController:(UIViewController  *)presented
                                                               presentingViewController:(nullable  UIViewController  *)presenting
                                                                   sourceViewController:(UIViewController  *)source;
先来看这两个代理方法:animationControllerForPresentedController:presentingController:sourceController:(UIViewController *)source和animationControllerForDismissedController:(UIViewController *)dismissed他们都返回了一个实现UIViewControllerAnimatedTransitioning协议的对象。UIViewControllerAnimatedTransitioning协议的方法就是我们实现动画的地方。关于此协议我们等会再说。
再来看如下两个方法:interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator和interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator,他们都返回了一个实现UIViewControllerInteractiveTransitioning协议的对象,而此协议主要用于交互式转场动画。关于此协议我们等会再说。
push/pop 在使用UINavigationController进行页面之间的导航管理时,系统也是默认为我们实现了push和pop动画的,而如果想自定义push/pop动画,则需要一个实现UINavigationControllerDelegate协议的对象。如下:@interface  PushAViewController  ()
@property  (nonatomic , strong ) PushManager *pushManager;
@end 
@implementation  PushAViewController 
- (void )viewDidLoad {
    [super  viewDidLoad];
    self .view.backgroundColor = [UIColor  redColor];
}
- (void )touchesBegan:(NSSet <UITouch  *> *)touches withEvent:(UIEvent  *)event {
    PushBViewController *bVC = [[PushBViewController alloc] init];
    self .pushManager = [[PushManager alloc] init];
    bVC.navigationController.delegate = self .pushManager;
    [self .navigationController pushViewController:bVC animated:YES ];
}
@end 
UINavigationControllerDelegate 我们再来看看UINavigationControllerDelegate协议的相关方法:
- (nullable  id  <UIViewControllerInteractiveTransitioning >)navigationController:(UINavigationController  *)navigationController
                                   interactionControllerForAnimationController:(id  <UIViewControllerAnimatedTransitioning >) animationController;
- (nullable  id  <UIViewControllerAnimatedTransitioning >)navigationController:(UINavigationController  *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation )operation
                                                         fromViewController:(UIViewController  *)fromVC
                                                           toViewController:(UIViewController  *)toVC;
我们会发现 此两种方法分别返回了实现了UIViewControllerInteractiveTransitioning协议的对象和实现了UIViewControllerAnimatedTransitioning协议的对象。我们不难发现,无论是push/pop还是present/dismiss真正的执行动画都是一个实现了UIViewControllerAnimatedTransitioning协议的对象,而真正执行交互式动画的都是一个实现了UIViewControllerInteractiveTransitioning协议的对象。既然如此,那我们就来研究一下UIViewControllerAnimatedTransitioning和UIViewControllerInteractiveTransitioning协议
UIViewControllerAnimatedTransitioning 先来看此协议的方法:
- (NSTimeInterval )transitionDuration:(nullable  id  <UIViewControllerContextTransitioning >)transitionContext;
- (void )animateTransition:(id  <UIViewControllerContextTransitioning >)transitionContext;
此协议的两个方法必须实现。transitionDuration:方法返回动画执行的时间。通常情况下,系统执行present/dismiss和push/pop动画的时间为0.5秒左右。animateTransition方法就是真正执行动画的地方。这个方法系统会给我们传过来一个实现了UIViewControllerContextTransitioning协议的对象(转场上下文),在执行动画之前,我们先来了解一下UIViewControllerContextTransitioning协议。
先来看UIViewControllerContextTransitioning协议的定义
- (UIView  *)containerView;
- (BOOL )isAnimated;
- (BOOL )isInteractive; 
- (BOOL )transitionWasCancelled;
- (UIModalPresentationStyle )presentationStyle;
- (void )updateInteractiveTransition:(CGFloat )percentComplete;
- (void )finishInteractiveTransition;
- (void )cancelInteractiveTransition;
- (void )pauseInteractiveTransition NS_AVAILABLE_IOS (10 _0);
- (void )completeTransition:(BOOL )didComplete;
- (nullable  __kindof UIViewController  *)viewControllerForKey:(UITransitionContextViewControllerKey )key;
- (nullable  __kindof UIView  *)viewForKey:(UITransitionContextViewKey )key NS_AVAILABLE_IOS (8 _0);
- (CGAffineTransform )targetTransform NS_AVAILABLE_IOS (8 _0);
- (CGRect )initialFrameForViewController:(UIViewController  *)vc;
- (CGRect )finalFrameForViewController:(UIViewController  *)vc;
了解完UIViewControllerContextTransitioning协议后,我们就可以实现自定义转场动画了。
第一节中,我们定义了类PresentManager并实现了UIViewControllerAnimatedTransitioning和UIViewControllerTransitioningDelegate,我们先来看UIViewControllerTransitioningDelegate协议的具体实现
- (nullable  id  <UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController  *)presented
                                                                            presentingController:(UIViewController  *)presenting
                                                                                sourceController:(UIViewController  *)source {
    
    self .transitionStyle = TransitionStylePresent;
    return  self ;
}
- (nullable  id  <UIViewControllerAnimatedTransitioning >)animationControllerForDismissedController:(UIViewController  *)dismissed {
    
    self .transitionStyle = TransitionStyleDismiss;
    return  self ;
}
 
再来看看UIViewControllerAnimatedTransitioning协议的具体实现:- (NSTimeInterval )transitionDuration:(nullable  id  <UIViewControllerContextTransitioning >)transitionContext {
	
    return  1.0 ;
}
- (void )animateTransition:(id  <UIViewControllerContextTransitioning >)transitionContext {
    
    UIView   *containerView = [transitionContext containerView];
    if  (!containerView) {
        return ;
    }
    if  (self .transitionStyle == TransitionStylePresent) {
        
        
        
        UIViewController  *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey ];
        UIViewController  *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey ];
        
        
        UIView  *fromView, *toView;
        if  ([transitionContext respondsToSelector:@selector (viewForKey:)]) {
            fromView = [transitionContext viewForKey:UITransitionContextFromViewKey ];
            toView = [transitionContext viewForKey:UITransitionContextToViewKey ];
        }else  {
            fromView = fromViewController.view;
            toView = toViewController.view;
        }
        fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
        toView.frame = [transitionContext finalFrameForViewController:toViewController];
        
        
        toView.frame = CGRectMake (-toView.frame.size.width, toView.frame.origin.y, toView.frame.size.width, toView.frame.size.height);
        
        
        [containerView addSubview:fromView];
        [containerView addSubview:toView];
        
        
        [UIView  animateWithDuration:[self  transitionDuration:transitionContext] animations:^{
            toView.frame = CGRectMake (0 , toView.frame.origin.y, toView.frame.size.width, toView.frame.size.height);
        } completion:^(BOOL  finished) {
            
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }else  {
        
        
        UIViewController  *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey ];
        UIViewController  *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey ];
        
        
        UIView  *fromView, *toView;
        if  ([transitionContext respondsToSelector:@selector (viewForKey:)]) {
            fromView = [transitionContext viewForKey:UITransitionContextFromViewKey ];
            toView = [transitionContext viewForKey:UITransitionContextToViewKey ];
        }else  {
            fromView = fromViewController.view;
            toView = toViewController.view;
        }
        fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
        toView.frame = [transitionContext finalFrameForViewController:toViewController];
        
        
        [containerView addSubview:toView];
        [containerView addSubview:fromView];
        
        
        [UIView  animateWithDuration:[self  transitionDuration:transitionContext] animations:^{
            fromView.frame = CGRectMake (0 , -fromView.frame.size.height, fromView.frame.size.width, fromView.frame.size.height);
        } completion:^(BOOL  finished) {
            
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
}
我这里只是做一个简单的动画,至于该如何做更复杂的动画,那就开动大家的大脑吧。
同样的,自定义push/pop动画也是如此,这里不再详解。
交互式动画 在前面,我们一直提到一个协议UIViewControllerInteractiveTransitioning,实现此协议,我们就能使用交互式转场动画。iOS7为我们提供了已经实现好此协议的类UIPercentDrivenInteractiveTransition,我们只需继承此类,便可实现交互式动画。为了方便,下面的例子依旧在present/dismiss转场基础上讲解,我们让PresentManager继承UIPercentDrivenInteractiveTransition,并且在PresentBViewController上添加pan手势,具体代码如下:
- (nullable  id  <UIViewControllerAnimatedTransitioning >)animationControllerForPresentedController:(UIViewController  *)presented
                                                                            presentingController:(UIViewController  *)presenting
                                                                                sourceController:(UIViewController  *)source {
    
    self .transitionStyle = TransitionStylePresent;
    
    [self  addGesture:presented];
    self .presentingVC = presented;
    return  self ;
}
- (nullable  id  <UIViewControllerInteractiveTransitioning >)interactionControllerForDismissal:(id  <UIViewControllerAnimatedTransitioning >)animator {
    if  (self .interacting) {
        return  self ;
    }
    return  nil ;
}
- (void )addGesture:(UIViewController  *)viewController {
    UIPanGestureRecognizer  *pan = [[UIPanGestureRecognizer  alloc] initWithTarget:self  action:@selector (panGesture:)];
    [viewController.view addGestureRecognizer:pan];
}
- (void )panGesture:(UIPanGestureRecognizer  *)gesture {
    CGPoint  translation = [gesture translationInView:gesture.view];
    switch  (gesture.state) {
        case  UIGestureRecognizerStateBegan :
        		
            self .interacting = YES ;
            [self .presentingVC dismissViewControllerAnimated:YES  completion:nil ];
            break ;
        case  UIGestureRecognizerStateChanged : {
        		
            CGFloat  fraction = translation.x / gesture.view.frame.size.width;
            [self  updateInteractiveTransition:fraction];
            break ;
        }
        case  UIGestureRecognizerStateEnded :
        case  UIGestureRecognizerStateCancelled : {
        		
            self .interacting = NO ;
            CGFloat  fraction = translation.x / gesture.view.frame.size.width;
            if  (fraction<0.5  || gesture.state == UIGestureRecognizerStateCancelled ) {
                [self  cancelInteractiveTransition];
            } else  {
                [self  finishInteractiveTransition];
            }
            break ;
        }
        default :
            break ;
    }
}
 
同样的,push/pop亦是如此。
UIPresentationController 在第一节,我们说到UIViewControllerTransitioningDelegate协议的时候,还有个方法没有说,它是iOS8以后才有的,此方法为:- (nullable  UIPresentationController  *)presentationControllerForPresentedViewController:(UIViewController  *)presented
                                                               presentingViewController:(nullable  UIViewController  *)presenting
                                                                   sourceViewController:(UIViewController  *)source
此方法返回一个UIPresentationController对象,这个对象是做什么的呢?
UIViewController有一个属性modalPresentationStyle,我们来看看它有哪些值:typedef  NS_ENUM (NSInteger , UIModalPresentationStyle ) {
        UIModalPresentationFullScreen  = 0 ,
        UIModalPresentationPageSheet  NS_ENUM_AVAILABLE_IOS (3 _2) __TVOS_PROHIBITED,
        UIModalPresentationFormSheet  NS_ENUM_AVAILABLE_IOS (3 _2) __TVOS_PROHIBITED,
        UIModalPresentationCurrentContext  NS_ENUM_AVAILABLE_IOS (3 _2),
        UIModalPresentationCustom  NS_ENUM_AVAILABLE_IOS (7 _0),
        UIModalPresentationOverFullScreen  NS_ENUM_AVAILABLE_IOS (8 _0),
        UIModalPresentationOverCurrentContext  NS_ENUM_AVAILABLE_IOS (8 _0),
        UIModalPresentationPopover  NS_ENUM_AVAILABLE_IOS (8 _0) __TVOS_PROHIBITED,
        UIModalPresentationNone  NS_ENUM_AVAILABLE_IOS (7 _0) = -1 ,         
};
平常开发中,我们使用最多的就是UIModalPresentationCustom,如果我们不使用UIModalPresentationCustom,默认的系统会在我们调用“上下文”的completeTransition方法后会把fromVC移除掉。
如果我们想在present/pop执行动画的生命周期过程中,任意的在上下文中插入视图或者更改最终视图的大小等,使用UIPresentationController便可实现。我们来看UIPresentationController类的定义:
@property (nullable , nonatomic , readonly , strong ) UIView  *containerView;
- (void )containerViewWillLayoutSubviews;
- (void )containerViewDidLayoutSubviews;
- (CGRect )frameOfPresentedViewInContainerView;
- (void )presentationTransitionWillBegin;
- (void )presentationTransitionDidEnd:(BOOL )completed;
- (void )dismissalTransitionWillBegin;
- (void )dismissalTransitionDidEnd:(BOOL )completed;
如此,我们便可在视图周期方法中任意的添加和删除视图,以满足我们的需求。也可以在布局过程中,改变弹出视图的frame。切记,视图的添加和删除都是在上下文的view中进行的。
小结 了解完整个转场动画的框架,我们合理的使用框架中的协议和类,便能尽可能的满足我们的开发需求。至于该如何实现,大家可以动脑了~