通过自定义过渡动画实现翻转卡片效果(Swift 3)
译者:教程和相关代码已经更新至兼容Swift 3.0,原教程中Bug已经清扫完毕,升级到Xcode 8的程序猴们可以放心观看。Swift3对CoreAnimation的API进行了大量更新,让代码简洁了不少。关于Swift3的更新,可以参考我的Swift3的变化。
推入、弹出、翻转….iOS内置了不少视图间的过渡动画,但自己动手显然更有趣些。自定义的过渡动画不但可以大大增强用户体验,也可以让你的App“鹤立鸡群”。如果你是一名曾经被自定义动画的复杂吓跑过的老司机,不妨也停下来看看,现在的实现方法比你想象的要简单的多。
本篇教程里,我们将给一个简单的猜猜看游戏添加过渡动画。教程结束时你将获得以下技能:
- 了解过渡动画API的结构
- 了解如何通过自定义过渡动画打开/关闭视图控制器
- 了解如何创建交互式过渡
提示:本篇教程会用到UIView的动画方法,你应该具备基本的知识储备。如果没有接触过的话建议提前看一下这这篇关于iOS动画的文章,速成一下。
入门
老规矩,Clone或下载初始项目,编译运行一下,效果如下所示:
我们在一个Page View Controller里展示了不同的卡片。每张卡片上有一段关于宠物的描述,点击卡片会显示对应的宠物照片。
我们的任务是根据描述猜出宠物!它是喵星人,汪星人还是条咸鱼?自己把玩一下初始App看看你猜的准不准。
主要的导航逻辑为你已经写好了,但现在这个App太过普通了,没什么意思。我们通过自定义的过渡动画给它增添点色彩。
探索过渡动画API
过渡动画API大量地使用协议,而非实体对象。看完这一部分内容,你会了解每一个相关协议的职责,以及它们之间是如何互相联系的。下图展示了API中的一些重要主体:
相关主体
上图看起来挺复杂的,但当你了解了不同部分之间是如何协同工作之后,你会发现它的逻辑其实非常直接。
过渡协议
每一个视图控制器(View Controller)都包含一个transitioningDelegate对象,它服从UIViewControllerDelegate协议。
每当你打开或关闭一个视图控制器时,UIKit会向这个协议索取应该使用的动画控制器(Animation Controller)。如果想让协议获得的是我们自定义的对象,只需将它赋值给视图控制器的transitioningDelegate。
动画控制器
具体实现UIViewControllerAnimatedTransitioning协议的对象,用来实现过渡动画。
过渡上下文
上下文负责实现UIViewControllerContextTransitioning协议,它在过渡过程中至关重要:它负责封装所有与过渡相关的视图控制器(过渡前和过渡后的)。
实际上我们并不需要自己实现这个协议。每当过渡发生时,动画控制器会自动从UIKit那里接收到已经设置好的上下文对象。
过渡的步骤
打开一个视图控制器时要经历以下几步:
1. 通过代码或者Segue触发过渡。
2. UIKit尝试从目标视图控制器(即将展示的)那里获取它的过渡协议。如果为空,则使用内置的标准协议。
3. UIKit通过animationController(forPresented:presenting:source:)方法向过渡协议请求动画控制器。如果方法返回nil,则使用默认动画。
4. 如果上述方法返回有效,UIKit会创建过渡上下文。
5. UIKit通过transitionDuration(using:) 方法从动画控制器那里获取动画时长。
6. UIKit在动画控制器上调用animateTransition(using:)方法,实际播放过渡动画。
7. 最后,动画控制器会调用过渡上下文的completeTransition(using:)方法,标志动画的完成。
自定义打开动画
是时候把刚刚新学的知识应用到实际中了!
我们的目标是实现下面的效果:
- 当用户点击卡片,卡片翻转过来显示缩小版的Modal视图(和卡片一样大)
- 随后放大至充满屏幕
创建Animator
首先需要创建动画控制器。
创建一个新的Cocoa Touch Class文件,命名为FlipPresentAnimationController,让它继承自NSObject,语言设置为Swift。点击下一步,把分组设置为Animation Controllers,然后完成创建。
动画控制器需要遵从UIViewControllerAnimatedTransitioning协议。打开FlipPresentAnimationController.swift,更新类的声明:
importUIKit
classFlipPresentAnimationController:NSObject,UIViewControllerAnimatedTransitioning{
修改完声明后,编辑器会弹出血红的Error,提示缺失代理方法;淡定淡定,我们这不是啥都还没添加呢么,现在就来修正。

我们把卡片的框架(Frame)作为动画起点,并添加变量存储这个值:
varoriginFrame=CGRect.zero
根据协议要求,我们需要添加两个方法。
把下面的方法添加到类里:
functransitionDuration(using transitionContext:UIViewControllerContextTransitioning?)->TimeInterval{
return2.0
正如名称提示的一样,该方法用于设定动画时长。暂且把它设定为2秒,以便在开发过程中观察动画效果。
接着把下面的方法声明添加到类里:
funcanimateTransition(using transitionContext:UIViewControllerContextTransitioning){
我们需要在这个方法里真正实现动画。
首先添加下面代码:
// 1
letcontainerView=transitionContext.containerView
guard letfromVC=transitionContext.viewController(forKey:.from),
lettoVC=transitionContext.viewController(forKey:.to)else{
return
// 2
letinitialFrame=originFrame
letfinalFrame=transitionContext.finalFrame(for:toVC)
// 3
letsnapshot=toVC.view.snapshotView(afterScreenUpdates:true)!
snapshot.frame=initialFrame
snapshot.layer.cornerRadius=25
snapshot.layer.masksToBounds=true
解释一下:
1. 过渡上下文负责提供与过渡相关的视图控制器,通过对应的键获取。
2. 设置“to”视图的起始帧和终止帧。过渡动画中,它从卡片大小开始,逐渐扩充并填满整个屏幕。
3. UIView捕捉“to”视图快照,并把它渲染成轻量级的视图;这样就可以在动画中同时显示当前的视图和它的父视图。快照同样从卡片的边框大小开始。此外我们把快照的边角弧度改成和卡片一样。
译者:在iOS10(Swift3)环境下,
snaphotView(afterScreenUpdates:)方法无法正常获取快照。为了临时解决这个Bug,我们需要自己编写一个辅助方法,以图片的形式返回快照。
创建一个新的Swift文件,命名为Util,用来存放我们编写的扩展方法。添加下面代码:
importUIKit
extensionUIImage{
classfuncrenderImage(from view:UIView)->UIImage?{
UIGraphicsBeginImageContextWithOptions(view.frame.size,true,0)
letcontext=UIGraphicsGetCurrentContext()
view.layer.render(in:context!)
letrenderedImage=UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
returnrenderedImage
然后回到之前的animateTransition(using:)方法,把注释3下面的第一行替换成下面的代码:
// let snapshot = toVC.view.snapshotView(afterScreenUpdates: true)!
// Ed: snapshot在iOS 10(swift3)下无法正常获取
// 这里用图片代替快照
letsnapshot=UIImageView(image:UIImage.renderImage(from:toVC.view))
打完补丁我们接着往下进行。
往animateTransition(using:)方法里继续添加下面的代码:
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
toVC.view.isHidden=true
AnimationHelper.perspectiveTransformForContainerView(containerView)
snapshot.layer.transform=AnimationHelper.yRotation(M_PI_2)
这里出现了一个新家伙:容器视图(Container View),我们可以把它想象成过渡动画的舞台。容器视图自动包含了“from”视图,我们需要自行添加“to”视图。
此外,我们还需要把快照添加到容器视图里,并隐藏实际视图。动画结束时,快照也会旋转至消失。
提示:不要被
AnimationHelper唬住了,它只是一个工具类,负责给视图添加透视效果以及旋转变换。感兴趣的话可以看看它的具体实现。
现在,与实现动画相关的对象已经准备完毕。在方法的最后添加下面代码,具体实现动画效果:
// 1
letduration=transitionDuration(using:transitionContext)
UIView.animateKeyframes(
withDuration:duration,
delay:0,
options:.calculationModeCubic,
animations:{
// 2
UIView.addKeyframe(withRelativeStartTime:0.0,relativeDuration:1/3,animations:{
fromVC.view.layer.transform=AnimationHelper.yRotation(-M_PI_2)
// 3
UIView.addKeyframe(withRelativeStartTime:1/3,relativeDuration:1/3,animations:{
snapshot.layer.transform=AnimationHelper.yRotation(0.0)
// 4
UIView.addKeyframe(withRelativeStartTime:2/3,relativeDuration:1/3,animations:{
snapshot.frame=finalFrame
completion:{_in
// 5
toVC.view.isHidden=false
fromVC.view.layer.transform=AnimationHelper.yRotation(0.0)
snapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
解释一下:
1. 首先,我们设置了动画时长。注意这里transitionDuration(using:)的方法,我们在类的最开始实现了它。我们需要让动画时长和整个过渡的时长相同,以便UIKit进行同步。
2. 我们先把“from”视图沿着y轴翻转一半,让它离开画面(变成一条线了)。
3. 接着用同样的方法逐渐显示快照。
4. 然后我们让快照逐渐填满整个屏幕。
5. 一切准备完毕,我们可以放心地显示“to”视图了。快照的任务已经完成,可以把它删除了。此外我们还需要把“from”视图翻转回去,不然返回上级视图的时候就看不到它了。最后,调用completeTransition方法通知上下文,宣告动画已经完成。UIKit会确保最终状态的一致性,然后从容器里删除“from”视图。
我们的动画控制器现在可以投入使用了!
连接Animator
打开CardViewController.swift,在类外面添加下面的属性:
private letflipPresentAnimationController=FlipPresentAnimationController()
UIKit需要一个可以提供动画控制器的代理对象。因此,我们必须提供一个服从UIViewControllerTransitioningDelegate协议的对象。
在这里,我们把CardViewController当做过渡代理。在源文件末尾添加协议扩展:
extensionCardViewController: UIViewControllerTransitioningDelegate{
在扩展里添加下面的方法:
funcanimationController(forPresented presented:UIViewController,presenting:UIViewController,source:UIViewController)->UIViewControllerAnimatedTransitioning?{
flipPresentAnimationController.originFrame=cardView.frame
returnflipPresentAnimationController
上面的方法返回了自定义的动画控制器,同时也确保了过渡动画从正确的帧开始。
最后一步是把CardViewController设置为过渡代理。视图控制器包含一个transitioningDelegate属性,UIKit正是通过它来确定是否使用自定义过渡。
在prepare(for segue:sender:)方法里添加下面的代码,就在给卡片赋值的下面:
destinationViewController.transitioningDelegate=self
注意,需要代理协议的是即将展示的视图控制器,而不是负责展示的。
编译运行一下我们的项目,点击卡片应该会显示如下的效果:
搞定!你的一个自定义过渡动画。别着急得意,这仅仅是一半而已:我们需要用同样浮夸的方式关闭视图。
自定义关闭动画
创建一个新的Cocoa Touch类,命名为FlipDismissAnimationController,确保它继承自NSObject并隶属于Animation Controllers组下。
往新文件里添加下面的代码:
importUIKit
classFlipDismissAnimationController:NSObject,UIViewControllerAnimatedTransitioning{
vardestinationFrame=CGRect.zero
functransitionDuration(using transitionContext:UIViewControllerContextTransitioning?)->TimeInterval{
return0.6
funcanimateTransition(using transitionContext:UIViewControllerContextTransitioning){
这个类需要实现打开动画的逆转版:
- 把当前页面缩小为卡片大小;我们用
destinationFrame存储这个值。 - 翻转视图以显示原始卡片。
在animateTransition(_:)方法里添加下面的代码:
letcontainerView=transitionContext.containerView
guard letfromVC=transitionContext.viewController(forKey:.from),
lettoVC=transitionContext.viewController(forKey:.to)
else{
return
// 1
letfinalFrame=destinationFrame
// 2
letsnapshot=UIImageView(image:UIImage.renderImage(from:fromVC.view))
snapshot.layer.cornerRadius=25
snapshot.layer.masksToBounds=true
// 3
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot)
fromVC.view.isHidden=true
AnimationHelper.perspectiveTransformForContainerView(containerView)
// 4
toVC.view.layer.transform=AnimationHelper.yRotation(-M_PI_2)
letduration=transitionDuration(using:transitionContext)
你应该已经熟悉这些步骤了,这里还是再重复解释一下:
1. 因为在这个动画里,我们需要缩小视图,所以初始帧和终止帧和之前正好相反。
2. 这次我们操作的是“from”视图,所以快照应该通过它来创建。
3. 和之前一样,我们把“to”视图以及快照添加到容器视图里,并隐藏“from”视图以免和快照冲突。
4. 最后,我们通过翻转隐藏“to”视图。
剩下的就是添加动画本身了。
紧接着之前的代码,在animateTransition(_:)里添加下面的代码:
letduration=transitionDuration(using:transitionContext)
UIView.animateKeyframes(
withDuration:duration,
delay:0,
options:.calculationModeCubic,
animations:{
// 1
UIView.addKeyframe(withRelativeStartTime:0.0,relativeDuration:1/3,animations:{
snapshot.frame=finalFrame
UIView.addKeyframe(withRelativeStartTime:1/3,relativeDuration:1/3,animations:{
snapshot.layer.transform=AnimationHelper.yRotation(M_PI_2)
UIView.addKeyframe(withRelativeStartTime:2/3,relativeDuration:1/3,animations:{
toVC.view.layer.transform=AnimationHelper.yRotation(0.0)
completion:{_in
// 2
fromVC.view.isHidden=false
snapshot.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
这就是之前动画的逆转版本:
1. 首先缩放视图,然后通过翻转隐藏快照。接着通过反方向旋转逐渐显示“to”视图。
2. 最后,我们删除快照并通知上下文,过渡动画已经完成。这样UIKit就知道可以开始更新视图控制器的层级结构,并删除过渡时使用的视图了。
打开CardViewController.swift,在动画控制器下面添加一个新的属性:
private letflipDismissAnimationController=FlipDismissAnimationController()
接着,往协议扩展里添加下面的方法:
funcanimationController(forDismissed dismissed:UIViewController)->UIViewControllerAnimatedTransitioning?{
flipDismissAnimationController.destinationFrame=cardView.frame
returnflipDismissAnimationController
和之前类似,负责把正确的视图框架传递给动画控制器,并返回这个控制器。
最后一步,修改FlipPresentAnimationController里的transitionDuration(using:)方法,把它的速度改成和关闭动画一致:
functransitionDuration(using transitionContext:UIViewControllerContextTransitioning?)->TimeInterval{
return0.6
编译运行你的App,点击卡片看看现在的打开/关闭动画:
非常犀利的自定义动画!但为了追求完美,我们更进一步,给动画添加交互。
添加交互
译者:如果你看过我之前几篇教程,这部分内容可以跳过。不妨回忆一下思路,尝试独立完成。
iOS自带的设置软件是交互式过渡动画的典范。这一节我们的任务是,利用左边缘滑动手势,退回卡片朝下的状态,过渡动画跟随用户手势。
交互式过渡的原理
交互控制器(Interaction Controller)可以响应触摸事件以及编码控制,比如加速、减速,甚至反转过渡动画。为了添加交互,过渡代理必须负责额外提供一个交互控制器。它可以是任何实现了UIViewControllerInteractiveTransitioning协议的对象。我们已经编写好了过渡动画,交互控制器只是负责让这个动画跟随你的手势,而不是像播放视频一样,从头到尾直接放完。
Apple提供了一个现成的UIPercentDrivenInteractiveTransition类,它是一个具体的交互控制器的实现。我们正好可以利用它,给我们的过渡添加交互。
创建交互式过渡
首先我们需要创建一个交互控制器。新建一个Cocoa Touch Class文件,命名为SwipeInteractionController,让它继承自UIPercentDrivenInteractiveTransition。确保语言为Swift,添加到Interaction Controllers组里。
打开SwipeInteractionController.swift,在类定义的最开始添加下面这些属性:
varinteractionInProgress=false
private varshouldCompleteTransition=false
private weakvarviewController:UIViewController!
这些属性的作用显而易见:
- 正如其名,
interactionInProgress用于指示交互是否在进行中。 - 我们需要在内部使用
shouldCompleteTransition来控制过渡,随后你会看到使用方法。 - 交互控制器直接负责打开/关闭视图控制器,所以我们需要把当前的视图控制器存储在
viewController里,便于引用。
把下面的方法添加到类里:
funcwire(toviewController:UIViewController!){
self.viewController=viewController
prepareGestureRecognizer(in:viewController.view)
我们需要通过手势控制过渡动画。在上面的方法里,我们获得了视图控制器的引用,并给它的视图添加了手势识别器。
如下添加prepareGestureRecognizerInView(_:)方法:
private funcprepareGestureRecognizer(inview:UIView){
letgesture=UIScreenEdgePanGestureRecognizer(target:self,action:#selector(handleGesture(gestureRecognizer:)))
gesture.edges=.left
view.addGestureRecognizer(gesture)
我们定义了一个手势识别器,通过左边缘滑动手势触发,并把它添加到视图上。
最后一步是添加handleGesture(_:)方法,如下所示:
funchandleGesture(gestureRecognizer:UIScreenEdgePanGestureRecognizer){
// 1
lettranslation=gestureRecognizer.translation(in:gestureRecognizer.view?.superview)
varprogress=Float(translation.x/200)
progress=fminf(fmaxf(progress,0.0),1.0)
switchgestureRecognizer.state{
case.began:
// 2
interactionInProgress=true
viewController.dismiss(animated:true,completion:nil)
case.changed:
// 3
shouldCompleteTransition=progress>0.5
update(CGFloat(progress))
case.cancelled:
// 4
interactionInProgress=false
cancel()
case.ended:
// 5
interactionInProgress=false
if!shouldCompleteTransition{cancel()}else{finish()}
default:
print("Unsupported")
解释一下:
1. 首先我们定义了一个用于追踪进度的局部变量。我们会记录手势的位移,并计算进度。滑动200p代表100%完成,以此衡量进度。
2. 手势开始后,我们调整interactionInProgress的值并触发关闭视图控制器的操作。
3. 手势进行时,我们不断调用update方法更新进度。它是UIPercentDrivenInteractiveTransition的一个方法,根据你传入的百分比值更新过渡动画。
4. 如果手势被取消,更新interactionInProgress的值,并回滚过渡动画。
5. 手势完成后,根据当前进度判断是取消还是完成过渡动画。

打开CardViewController.swift 添加交互控制器:
private letswipeInteractionController=SwipeInteractionController()
UIKit会通过interactionControllerForDismissal(_:)方法获取交互控制器。
在扩展里添加该方法的实现:
funcinteractionControllerForDismissal(using animator:UIViewControllerAnimatedTransitioning)->UIViewControllerInteractiveTransitioning?{
returnswipeInteractionController.interactionInProgress?swipeInteractionController:nil
上面的代码检查了视图是否处于手势识别的过程中,即交互是否进行中。然后根据结果返回对应的交互控制器。
现在跳转到prepare(for:sender:)方法,在设定transitioningDelegate的语句下面添加下面的代码:
swipeInteractionController.wire(to:destinationViewController)
它负责把交互控制器和当前的视图控制器相连。
编译运行一下我们的项目,点击卡片,然后从屏幕左侧滑动试试看。
恭喜恭喜,你已成功解锁这个犀利的交互式过渡动画。
总结
你可以在这里下载完整的项目。
本篇教程着重介绍的是Modal视图的打开关闭过渡动画。值得注意的是,自定义UIViewController过渡动画的方法同样适用于容器型的视图控制器:
使用导航控制器(Navigation View Controller)时,动画控制器由代理负责,即实现了UINavigationControllerDelegate方法的对象。它可以通过navigationController(_:animationControllerFor:from:to:)方法提供动画控制器。
标签栏控制器(Tab Bar Controller)依赖于实现UITabBarControllerDelegate协议的对象,通过tabBarController(_:animationControllerForTransitionFrom:to:)方法返回动画控制器。
原文链接:https://www.raywenderlich.com/110536/custom-uiviewcontroller-transitions
第一时间获取iOS优质中文教程,扫描下方的二维码订阅程序猴猴猴,或订阅我的博客:www.jiarui-blog.com。
自定义过渡动画(swift 3)






