本文翻译总结自AppCoda以下两篇文章:
iOS 7开始,苹果为开发者提供了自定义控制器转场动画相关的API,而实现该功能需要以下三个步骤:
创建一个类作为动画管理器,该类需继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议,我们在这个类中编写我们的动画执行代码。 
为目标控制器指定转场动画代理,既可以使用上一步创建的动画管理器对象,也可以指定来源控制器作为这个代理。 
实现代理协议中的相应方法,在方法中返回第一步创建的动画管理器对象。 
 
准备工作 
下载示例程序,地址在这里 。(译注:原文地址需要FQ访问,本人已转存到GitHub上,点击这里 。)
示例程序如下图所示,点击导航栏上的Action按钮会modal出一个目标控制器,点击Dismiss按钮会返回来源控制器,只不过现在使用的是系统默认的modal动画,接下来我们就来实现自定义转场动画。
创建动画管理器 
创建一个类名称为CustomPresentAnimationController,继承自NSObject并遵守UIViewControllerAnimatedTransitioning协议。这个协议有两个必须实现的方法,我们的实现代码如下:
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
func  transitionDuration ( transitionContext :  UIViewControllerContextTransitioning )  ->  NSTimeInterval  { 
    return  2.5 
 } 
 func  animateTransition ( transitionContext :  UIViewControllerContextTransitioning )  { 
     let  fromViewController  =  transitionContext . viewControllerForKey ( UITransitionContextFromViewControllerKey ) ! 
     let  toViewController  =  transitionContext . viewControllerForKey ( UITransitionContextToViewControllerKey ) ! 
     let  finalFrameForVC  =  transitionContext . finalFrameForViewController ( toViewController ) 
     let  containerView  =  transitionContext . containerView () 
     let  bounds  =  UIScreen . mainScreen (). bounds 
     toViewController . view . frame  =  CGRectOffset ( finalFrameForVC ,  0 ,  bounds . size . height ) 
     containerView . addSubview ( toViewController . view ) 
 
     UIView . animateWithDuration ( transitionDuration ( transitionContext ),  delay :  0.0 ,  usingSpringWithDamping :  0.5 ,  initialSpringVelocity :  0.0 ,  options :  . CurveLinear ,  animations :  { 
         fromViewController . view . alpha  =  0.5 
         toViewController . view . frame  =  finalFrameForVC 
         },  completion :  { 
             finished  in 
             transitionContext . completeTransition ( true ) 
             fromViewController . view . alpha  =  1.0 
     }) 
 } 
 
第一个方法很简单,设定动画执行时间。第二个方法则用来编写我们自定义的动画代码,在这个方法中我们可以利用transitionContext(转场上下文)来获得我们将来的来源控制器、目标控制器、动画完成后的最终frame,还可以获得用来管理来源或目标视图的容器视图。
然后我们将目标视图调整到屏幕下方并将其添加到容器视图内。接下来在动画执行的闭包内,将目标视图的位置变为最终位置,并将来源视图的透明度降为0.5,使其在目标视图进入的过程中产生一个淡出的效果。在动画完成的闭包内,我们告知transitionContext动画已完成,并将来源视图的透明度改回1.0。
设置转场动画代理 
接下来我们需要为目标控制器设置转场动画代理,这里我们指定来源控制器作为我们的代理。在ItemsTableViewController中,让其遵守UIViewControllerTransitioningDelegate协议,在storyboard中找到我们modal的segue,设置它的Identifier为showAction。然后在ItemsTableViewController中添加如下代码:
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
let  customPresentAnimationController  =  CustomPresentAnimationController () 
 override  func  prepareForSegue ( segue :  UIStoryboardSegue ,  sender :  AnyObject ? )  { 
     if  segue . identifier  ==  "showAction"  { 
         let  toViewController  =  segue . destinationViewController  as  UIViewController 
         toViewController . transitioningDelegate  =  self 
     } 
 } 
 func  animationControllerForPresentedController ( presented :  UIViewController ,  presentingController  presenting :  UIViewController ,  sourceController  source :  UIViewController )  ->  UIViewControllerAnimatedTransitioning ?  { 
    return  customPresentAnimationController 
 } 
 
我们创建了一个动画管理器对象,设置目标控制器的转场代理为来源控制器,然后实现代理协议中的animationControllerForPresentedController方法,该方法用于指定modal过程中展示视图的动画,在该方法中返回我们自定义的动画管理器对象。
运行我们的程序,效果如下图所示:
跟系统默认modal效果差不多,不过带有弹簧效果。如果你希望有不同的效果,你可以对下面这句代码进行修改。
1 
toViewController . view . frame  =  CGRectOffset ( finalFrameForVC ,  0 ,  bounds . size . height ) 
 
比如将其改为如下代码:
1 
toViewController . view . frame  =  CGRectOffset ( finalFrameForVC ,  0 ,  - bounds . size . height ) 
 
再次运行程序,我们的modal动画就变为从上往下了。
自定义modal过程中退出视图的动画 
我们的程序现在点击Dismiss退出目标控制器时,仍然是系统默认的动画,接下来实现这个自定义动画。
步骤同前面基本一样,创建一个叫做CustomDismissAnimationController的动画管理器,实现如下代理方法:
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
func  transitionDuration ( transitionContext :  UIViewControllerContextTransitioning )  ->  NSTimeInterval  { 
    return  2 
 } 
 func  animateTransition ( transitionContext :  UIViewControllerContextTransitioning )  { 
    let  fromViewController  =  transitionContext . viewControllerForKey ( UITransitionContextFromViewControllerKey ) ! 
     let  toViewController  =  transitionContext . viewControllerForKey ( UITransitionContextToViewControllerKey ) ! 
     let  finalFrameForVC  =  transitionContext . finalFrameForViewController ( toViewController ) 
     let  containerView  =  transitionContext . containerView () 
     toViewController . view . frame  =  finalFrameForVC 
     toViewController . view . alpha  =  0.5 
     containerView . addSubview ( toViewController . view ) 
     containerView . sendSubviewToBack ( toViewController . view ) 
 
     UIView . animateWithDuration ( transitionDuration ( transitionContext ),  animations :  { 
         fromViewController . view . frame  =  CGRectInset ( fromViewController . view . frame ,  fromViewController . view . frame . size . width  /  2 ,  fromViewController . view . frame . size . height  /  2 ) 
         toViewController . view . alpha  =  1.0 
     },  completion :  { 
         finished  in 
         transitionContext . completeTransition ( true ) 
     }) 
 } 
 
这次我们使用一个新的动画方式,让来源视图从中心点开始逐渐变小直到消失。首先我们将目标控制器设置为最终位置,透明度为0.5,并将其添加到容器视图的底层中使其开始时不可见。在动画执行过程中,来源视图逐渐变小,露出底层的目标视图,并将目标视图透明度过渡到1.0。
接下来在ItemsTableViewController中添加如下代码:
1 
2 
3 
4 
5 
let  customDismissAnimationController  =  CustomDismissAnimationController () 
 func  animationControllerForDismissedController ( dismissed :  UIViewController )  ->  UIViewControllerAnimatedTransitioning ?  { 
    return  customDismissAnimationController 
 } 
 
animationControllerForDismissedController这个代理方法指定了modal过程中退出视图的动画。运行程序,你会发现我们的动画有点小Bug。
我们可以看到,白色的背景视图确实如我们所愿从中心点逐渐缩小,但是图片视图的大小却保持不变,这是因为改变来源视图的时候,它的子控件的大小并不会跟着发生改变,我们可以通过视图快照的技术来解决这一问题。
将animateTransition方法的实现修改为如下代码:
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
func  animateTransition ( transitionContext :  UIViewControllerContextTransitioning )  { 
    let  fromViewController  =  transitionContext . viewControllerForKey ( UITransitionContextFromViewControllerKey ) ! 
     let  toViewController  =  transitionContext . viewControllerForKey ( UITransitionContextToViewControllerKey ) ! 
     let  finalFrameForVC  =  transitionContext . finalFrameForViewController ( toViewController ) 
     let  containerView  =  transitionContext . containerView () 
     toViewController . view . frame  =  finalFrameForVC 
     toViewController . view . alpha  =  0.5 
     containerView . addSubview ( toViewController . view ) 
     containerView . sendSubviewToBack ( toViewController . view ) 
 
     let  snapshotView  =  fromViewController . view . snapshotViewAfterScreenUpdates ( false ) 
     snapshotView . frame  =  fromViewController . view . frame 
     containerView . addSubview ( snapshotView ) 
 
     fromViewController . view . removeFromSuperview () 
 
     UIView . animateWithDuration ( transitionDuration ( transitionContext ),  animations :  { 
         snapshotView . frame  =  CGRectInset ( fromViewController . view . frame ,  fromViewController . view . frame . size . width  /  2 ,  fromViewController . view . frame . size . height  /  2 ) 
         toViewController . view . alpha  =  1.0 
     },  completion :  { 
         finished  in 
         snapshotView . removeFromSuperview () 
         transitionContext . completeTransition ( true ) 
     }) 
 } 
 
我们给来源视图生成了一个快照,将它添加到容器视图中利用它来做动画,并将来源视图从父控件中移除。再次运行程序,我们的动画效果就正常了。
导航控制器的转场动画 
在UITabBarController和UINavigationController的管理下,你无需为每个目标控制器都设置转场代理,可以直接设置UITabBarControllerDelegate或UINavigationControllerDelegate即可。
接下来我们演示如何为导航控制器设置自定义转场动画。首先,仍然是创建一个动画管理器类叫做CustomNavigationAnimationController,然后实现UIViewControllerAnimatedTransitioning协议的方法。这里的动画代码采用的是一个开源的三维旋转动画,读者可以到这里 自行研究。
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
38 
39 
40 
41 
42 
43 
44 
45 
46 
47 
var  reverse :  Bool  =  false 
 func  transitionDuration ( transitionContext :  UIViewControllerContextTransitioning )  ->  NSTimeInterval  { 
    return  1.5 
 } 
 func  animateTransition ( transitionContext :  UIViewControllerContextTransitioning )  { 
    let  containerView  =  transitionContext . containerView () 
     let  toViewController  =  transitionContext . viewControllerForKey ( UITransitionContextToViewControllerKey ) ! 
     let  fromViewController  =  transitionContext . viewControllerForKey ( UITransitionContextFromViewControllerKey ) ! 
     let  toView  =  toViewController . view 
     let  fromView  =  fromViewController . view 
     let  direction :  CGFloat  =  reverse  ?  - 1  :  1 
     let  const :  CGFloat  =  - 0.005 
 
     toView . layer . anchorPoint  =  CGPointMake ( direction  ==  1  ?  0  :  1 ,  0.5 ) 
     fromView . layer . anchorPoint  =  CGPointMake ( direction  ==  1  ?  1  :  0 ,  0.5 ) 
 
     var  viewFromTransform :  CATransform3D  =  CATransform3DMakeRotation ( direction  *  CGFloat ( M_PI_2 ),  0.0 ,  1.0 ,  0.0 ) 
     var  viewToTransform :  CATransform3D  =  CATransform3DMakeRotation ( - direction  *  CGFloat ( M_PI_2 ),  0.0 ,  1.0 ,  0.0 ) 
     viewFromTransform . m34  =  const 
     viewToTransform . m34  =  const 
 
     containerView . transform  =  CGAffineTransformMakeTranslation ( direction  *  containerView . frame . size . width  /  2.0 ,  0 ) 
     toView . layer . transform  =  viewToTransform 
     containerView . addSubview ( toView ) 
 
     UIView . animateWithDuration ( transitionDuration ( transitionContext ),  animations :  { 
         containerView . transform  =  CGAffineTransformMakeTranslation ( - direction  *  containerView . frame . size . width  /  2.0 ,  0 ) 
         fromView . layer . transform  =  viewFromTransform 
         toView . layer . transform  =  CATransform3DIdentity 
     },  completion :  { 
         finished  in 
         containerView . transform  =  CGAffineTransformIdentity 
         fromView . layer . transform  =  CATransform3DIdentity 
         toView . layer . transform  =  CATransform3DIdentity 
         fromView . layer . anchorPoint  =  CGPointMake ( 0.5 ,  0.5 ) 
         toView . layer . anchorPoint  =  CGPointMake ( 0.5 ,  0.5 ) 
 
         if  ( transitionContext . transitionWasCancelled ())  { 
             toView . removeFromSuperview () 
         }  else  { 
             fromView . removeFromSuperview () 
         } 
         transitionContext . completeTransition ( ! transitionContext . transitionWasCancelled ()) 
     }) 
 } 
 
注意这里我们添加了一个reverse变量,用来指定转场动画的方向,这样我们可以将导航控制器push和pop过程的动画封装在一个动画管理器中。
在ItemsTableViewController中更改它的声明使其遵守UINavigationControllerDelegate协议,在viewDidLoad方法中设置代理为自己navigationController?.delegate = self,然后添加如下代码:
1 
2 
3 
4 
5 
6 
let  customNavigationAnimationController  =  CustomNavigationAnimationController () 
 func  navigationController ( navigationController :  UINavigationController ,  animationControllerForOperation  operation :  UINavigationControllerOperation ,  fromViewController  fromVC :  UIViewController ,  toViewController  toVC :  UIViewController )  ->  UIViewControllerAnimatedTransitioning ?  { 
    customNavigationAnimationController . reverse  =  operation  ==  . Pop 
     return  customNavigationAnimationController 
 } 
 
上面这个导航控制器的代理方法用于指定push或pop时的转场动画,其中operation参数可以用来判断转场的方向。运行程序,如下图所示:
导航控制器的手势交互 
我们知道苹果官方为导航控制器添加了一个默认的手势交互,就是在屏幕左侧向右滑动可以返回上一界面并带有pop动画,接下来我们为我们的自定义动画添加手势交互。
手势交互的管理器需要遵守的是UIViewControllerInteractiveTransitioning协议,该协议需要实现startInteractiveTransition方法指定开始交互,不过苹果官方为我们提供了另一个已经实现该协议的交互管理器类UIPercentDrivenInteractiveTransition,并提供以百分比的形式来控制交互过程的功能,比如控制交互的更新、取消、完成等,我们直接使用它来实现我们的交互控制。
创建一个类叫做CustomInteractionController并继承自UIPercentDrivenInteractiveTransition,添加如下代码:
1 
2 
3 
4 
5 
6 
7 
8 
9 
10 
11 
12 
13 
14 
15 
16 
17 
18 
19 
20 
21 
22 
23 
24 
25 
26 
27 
28 
29 
30 
31 
32 
33 
34 
35 
36 
37 
var  navigationController :  UINavigationController ! 
var  shouldCompleteTransition  =  false 
var  transitionInProgress  =  false 
var  completionSeed :  CGFloat  { 
    return  1  -  percentComplete 
 } 
 func  attachToViewController ( viewController :  UIViewController )  { 
    navigationController  =  viewController . navigationController 
     setupGestureRecognizer ( viewController . view ) 
 } 
 private  func  setupGestureRecognizer ( view :  UIView )  { 
        view . addGestureRecognizer ( UIPanGestureRecognizer ( target :  self ,  action :  "handlePanGesture:" )) 
 } 
 func  handlePanGesture ( gestureRecognizer :  UIPanGestureRecognizer )  { 
    let  viewTranslation  =  gestureRecognizer . translationInView ( gestureRecognizer . view ! . superview ! ) 
     switch  gestureRecognizer . state  { 
     case  . Began : 
         transitionInProgress  =  true 
         navigationController . popViewControllerAnimated ( true ) 
     case  . Changed : 
         var  const  =  CGFloat ( fminf ( fmaxf ( Float ( viewTranslation . x  /  200.0 ),  0.0 ),  1.0 )) 
         shouldCompleteTransition  =  const  >  0.5 
         updateInteractiveTransition ( const ) 
     case  . Cancelled ,  . Ended : 
         transitionInProgress  =  false 
         if  ! shouldCompleteTransition  ||  gestureRecognizer . state  ==  . Cancelled  { 
             cancelInteractiveTransition () 
         }  else  { 
             finishInteractiveTransition () 
         } 
     default : 
         println ( "Swift switch must be exhaustive, thus the default" ) 
     } 
 } 
 
attachToViewController方法用于将来传入导航控制器的目标控制器,我们为目标控制器的整个view添加了滑动手势以便将来可以实现滑动返回的pop动画,在监听手势滑动的方法中,我们根据手势的状态做如下处理:
开始滑动:设置transitionInProgress为true,并开始执行导航控制器的pop返回。 
滑动过程中:更新交互过程的百分比,我们假设指定滑动200点即为交互完成。 
取消或结束:设置transitionInProgress为false,如果交互过程执行50%以上则认为交互完成。 
 
接来下来到我们的ItemsTableViewController,添加如下代码:
1 
let  customInteractionController  =  CustomInteractionController () 
 
然后修改我们之前实现的导航控制器的代理方法如下:
1 
2 
3 
4 
5 
6 
7 
func  navigationController ( navigationController :  UINavigationController ,  animationControllerForOperation  operation :  UINavigationControllerOperation ,  fromViewController  fromVC :  UIViewController ,  toViewController  toVC :  UIViewController )  ->  UIViewControllerAnimatedTransitioning ?  { 
    if  operation  ==  . Push  { 
         customInteractionController . attachToViewController ( toVC ) 
     } 
     customNavigationAnimationController . reverse  =  operation  ==  . Pop 
     return  customNavigationAnimationController 
 } 
 
当我们push一个目标控制器时,就为该目标控制器设定交互控制。最后实现导航控制器代理中的另一个方法用于指定交互控制器,代码如下:
1 
2 
3 
func  navigationController ( navigationController :  UINavigationController ,  interactionControllerForAnimationController  animationController :  UIViewControllerAnimatedTransitioning )  ->  UIViewControllerInteractiveTransitioning ?  { 
    return  customInteractionController . transitionInProgress  ?  customInteractionController  :  nil 
 } 
 
运行程序,如下图所示:
完整的示例程序链接地址请点击这里 。
推荐阅读:
实现下拉菜单的小Demo 
Demo实现效果如下图所示,下载完整的Demo代码请点击这里 。(译注:原文地址需要FQ访问,本人已转存到GitHub上,点击这里 。)
实现过程同我们前面讲的自定义转场动画过程一样,首先创建一个动画管理器类MenuTransitionManager,然后设置目标控制器的转场代理,这次我们使用动画管理器对象作为代理,所以MenuTransitionManager既遵守了UIViewControllerAnimatedTransitioning协议,也遵守了UIViewControllerTransitioningDelegate协议。动画的执行代码比较简单,只是通过改变transform控制来源和目标视图的上下移动,目标视图我们仍然使用了快照技术。
我们还为来源视图的快照添加了一个点击的手势,这样在显示下拉菜单后,除了点击相应的菜单选项,点击下部的快照也可以返回到主页视图。只不过点击手势的处理我们使用了代理设计模式,而点击手势的添加我们使用了Swift的属性观察器语法,读者可以自行研究学习。
最后,希望大家学的愉快!