# iOS 自定義頁面的切換動畫與交互動畫 By Swift
在iOS7之前,開發者為了尋求自定義Navigation Controller的Push/Pop動畫,只能受限于子類化一個UINavigationController,或是用自定義的動畫去覆蓋它。但是隨著iOS7的到來,Apple針對開發者推出了新的工具,以更靈活地方式管理UIViewController切換。
我把最終的Demo稍做修改,算是找了一個合適的應用場景,另外配上幾張美圖,拉拉人氣。
雖然是Swift的Demo,但是轉成Objective-C相當容易。
## 最終效果預覽:

## 自定義導航欄的Push/Pop動畫
為了在基于UINavigationController下做自定義的動畫切換,先建立一個簡單的工程,這個工程的rootViewController是一個UINavigationController,UINavigationController的rootViewController是一個簡單的UIViewController(稱之為主頁面),通過這個UIViewController上的一個Button能進入到下一個UIViewController中(稱之為詳情頁面),我們先在主頁面的ViewController上實現兩個協議:UINavigationControllerDelegate和**UIViewControllerAnimatedTransitioning**,然后在ViewDidLoad里面把navigationController的delegate設為self,這樣在導航欄Push和Pop的時候我們就知道了,然后用一個屬性記下是Push還是Pop,就像這樣:
~~~
func?navigationController(navigationController:?UINavigationController!, animationControllerForOperation operation:?UINavigationControllerOperation, fromViewController fromVC:?UIViewController!, toViewController toVC:?UIViewController!) ->?UIViewControllerAnimatedTransitioning! {
? ??navigationOperation?= operation
? ??return?self
}
~~~
這是iOS7的新方法,這個方法需要你提供一個UIViewControllerAnimatedTransitioning,那**UIViewControllerAnimatedTransitioning**到底是什么呢?
UIViewControllerAnimatedTransitioning是蘋果新增加的一個協議,其目的是在需要使用自定義動畫的同時,又不影響視圖的其他屬性,讓你把焦點集中在動畫實現的本身上,然后通過在這個協議的回調里編寫自定義的動畫代碼,即“切換中應該會發生什么”,負責切換的具體內容,任何實現了這一協議的對象被稱之為**動畫控制器。**你可以借助協議能被任何對象實現的這一特性,從而把各種動畫效果封裝到不同的類中,只要方便使用和管理,你可以發揮一切手段。我在這里讓主頁面實現動畫控制器也是可以的,因為它是導航欄的rootViewController,會一直存在,我只要在里面編寫自定義的Push和Pop動畫代碼就可以了:
~~~
//UIViewControllerTransitioningDelegate
func?transitionDuration(transitionContext:?UIViewControllerContextTransitioning!) ->?NSTimeInterval?{
? ??return?0.4
}
func?animateTransition(transitionContext:?UIViewControllerContextTransitioning!) {
? ??let?containerView = transitionContext.containerView()
? ??let?toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
? ??let?fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
? ??var?destView:?UIView!
? ??var?destTransform:?CGAffineTransform!
? ??if?navigationOperation?==?UINavigationControllerOperation.Push?{
? ? ? ? containerView.insertSubview(toViewController.view, aboveSubview: fromViewController.view)
? ? ? ? destView = toViewController.view
? ? ? ? destView.transform?=?CGAffineTransformMakeScale(0.1,?0.1)
? ? ? ? destTransform =?CGAffineTransformMakeScale(1,?1)
? ? }?else?if?navigationOperation?==?UINavigationControllerOperation.Pop?{
? ? ? ? containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
? ? ? ? destView = fromViewController.view
? ? ? ??//?如果IDE是Xcode6 Beta4+iOS8SDK,那么在此處設置為0,動畫將不會被執行(不確定是哪里的Bug)
? ? ? ? destTransform =?CGAffineTransformMakeScale(0.1,?0.1)
? ? }
? ??UIView.animateWithDuration(transitionDuration(transitionContext), animations: {
? ? ? ? destView.transform?= destTransform
? ? ? ? }, completion: ({completed?in
? ? ? ? ? ? transitionContext.completeTransition(true)
? ? ? ? }))
}
~~~
上面第一個方法返回動畫持續的時間,而下面這個方法才是具體需要實現動畫的地方。UIViewControllerAnimatedTransitioning的協議都包含一個對象:transitionContext,通過這個對象能獲取到切換時的上下文信息,比如從哪個VC切換到哪個VC等。我們從transitionContext獲取containerView,這是一個特殊的容器,切換時的動畫將在這個容器中進行;UITransitionContextFromViewControllerKey和UITransitionContextToViewControllerKey就是從哪個VC切換到哪個VC,容易理解;除此之外,還有直接獲取view的UITransitionContextFromViewKey和UITransitionContextToViewKey等。
我按Push和Pop把動畫簡單的區分了一下,Push時scale由小變大,Pop時scale由大變小,不同的操作,toViewController的視圖層次也不一樣。最后,在動畫完成的時候調用**completeTransition**,告訴transitionContext你的動畫已經結束,這是非常重要的方法,**必須調用**。在動畫結束時沒有對containerView的子視圖進行清理(比如把fromViewController的view移除掉)是因為transitionContext會自動清理,所以我們無須在額外處理。
注意一點,這樣一來會發現原來導航欄的交互式返回效果沒有了,如果你想用原來的交互式返回效果的話,在返回動畫控制器的delegate方法里返回nil,如:
~~~
if?operation?==?UINavigationControllerOperation.Push?{
? ??navigationOperation?= operation
? ??return?self
}
return?nil
~~~
然后在viewDidLoad里,Objective-C直接self.navigationController.interactivePopGestureRecognizer.delegat = self就行了,Swift除了要navigationController.interactivePopGestureRecognizer.delegate = self之外,還要在self上聲明實現了UIGestureRecognizerDelegate這個協議,雖然實際上你并沒有實現。
一個簡單的自定義導航欄Push/Pop動畫就完成了。
## 自定義Modal的Present/Dismiss動畫
自定義Modal的Present與Dismiss動畫與之前類似,都需要提供一個動畫管理器,我們用詳情頁面來展示一個Modal頁面,詳情頁面就作為動畫管理器:
~~~
func?transitionDuration(transitionContext:?UIViewControllerContextTransitioning!) ->?NSTimeInterval?{
? ??return?0.6
}
func?animateTransition(transitionContext:?UIViewControllerContextTransitioning!) {
? ??let?containerView = transitionContext.containerView()
? ??let?toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
? ??let?fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
? ??var?destView:?UIView!
? ??var?destTransfrom =?CGAffineTransformIdentity
? ??let?screenHeight =?UIScreen.mainScreen().bounds.size.height
? ??if?modalPresentingType ==?ModalPresentingType.Present {
? ? ? ? destView = toViewController.view
? ? ? ? destView.transform = CGAffineTransformMakeTranslation(0, screenHeight)
? ? ? ? containerView.addSubview(toViewController.view)
? ? }?else?if?modalPresentingType == ModalPresentingType.Dismiss {
? ? ? ? destView = fromViewController.view
? ? ? ? destTransfrom = CGAffineTransformMakeTranslation(0, screenHeight)
? ? ? ? containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
? ? }
? ??UIView.animateWithDuration(transitionDuration(transitionContext), delay:?0, usingSpringWithDamping:?0.6, initialSpringVelocity:?0,
? ? ? ? options:?UIViewAnimationOptions.CurveLinear, animations: {
? ? ? ? ? ? destView.transform?= destTransfrom
? ? ? ? }, completion: {completed?in
? ? ? ? ? ? transitionContext.completeTransition(true)
? ? })
}
~~~
動畫部分用了一個iOS7的彈簧動畫,usingSpringWithDamping的值設置得越小,彈的就越明顯,動畫的其他地方與之前類似,不一樣的是之前主頁面除了做動畫管理器之外,還實現了UINavigationControllerDelegate協議,因為我們是自定義導航欄的動畫,而在這里需要自定義Modal動畫就要實現另一個協議:**UIViewControllerTransitioningDelegate**,這個協議與之前的UINavigationControllerDelegate協議具有相似性,都是返回一個動畫管理器,iOS7的方法總共有四個,有兩個交互式的先不管,我們只需要實現另兩個即可:
~~~
func?animationControllerForPresentedController(presented:?UIViewController!, presentingController presenting:?UIViewController!, sourceController source:?UIViewController!) ->?UIViewControllerAnimatedTransitioning! {
? ? modalPresentingType =?ModalPresentingType.Present
? ??return?self
}
func?animationControllerForDismissedController(dismissed:?UIViewController!) ->?UIViewControllerAnimatedTransitioning! {
? ? modalPresentingType =?ModalPresentingType.Dismiss
? ??return?self
}
~~~
我同樣的用一個屬性記下是Present還是Dismiss,然后返回self。因為我是用的Storyboard,所以需要在prepareForSegue方法里設置一下transitionDelegate:
~~~
override?func?prepareForSegue(segue:?UIStoryboardSegue!, sender:?AnyObject!) {
? ??let?modal = segue.destinationViewController?as?UIViewController
? ? modal.transitioningDelegate =?self
}
~~~
對需要執行自定義動畫的VC設置transitionDelegate屬性即可。
如此一來,一個針對模態VC的自定義動畫也完成了。
## 自定義導航欄的交互式動畫
與動畫控制器類似,我們把實現了**UIViewControllerInteractiveTransitioning**協議的對象稱之為**交互控制器**,最常用的就是把交互控制器應用到導航欄的Back手勢返回上,而如果要實現一個自定義的交互式動畫,我們有兩種方式來完成:實現一個交互控制器,或者使用iOS提供的UIPercentDrivenInteractiveTransition類作交互控制器。
### 使用UIPercentDrivenInteractiveTransition
我們這里就用UIPercentDrivenInteractiveTransition來完成導航欄的交互式動畫。先看下UIPercentDrivenInteractiveTransition的定義:

實際上這個類就是實現了UIViewControllerInteractiveTransitioning協議的交互控制器,我們使用它就能夠輕松地為動畫控制器添加一個交互動畫。調用updateInteractiveTransition:更新進度;調用cancelInteractiveTransition取消交互,返回到切換前的狀態;調用finishInteractiveTransition通知上下文交互已完成,同completeTransition一樣。我們把交互動畫應用到詳情頁面Back回主頁面的地方,由于之前的動畫管理器的角色是主頁面擔任的,Navigation Controller的delegate同一時間只能有一個,那在這里交互控制器的角色也由主頁面來擔任。首先添加一個手勢識別器:
~~~
let?popRecognizer =?UIScreenEdgePanGestureRecognizer(target:?self, action:?Selector("handlePopRecognizer:"))
popRecognizer.edges?=?UIRectEdge.Left
self.navigationController.view.addGestureRecognizer(popRecognizer)
UIScreenEdgePanGestureRecognizer繼承于UIPanGestureRecognizer,能檢測從屏幕邊緣滑動的手勢,設置edges為left檢測左邊即可。然后實現handlePopRecognizer:
func?handlePopRecognizer(popRecognizer:?UIScreenEdgePanGestureRecognizer) {
? ??var?progress = popRecognizer.translationInView(navigationController.view).x?/?navigationController.view.bounds.size.width
? ? progress =?min(1.0,?max(0.0, progress))
? ??println("\(progress)")
? ??if?popRecognizer.state?==?UIGestureRecognizerState.Began?{
? ? ? ??println("Began")
? ? ? ??self.interactivePopTransition?=?UIPercentDrivenInteractiveTransition()
? ? ? ??self.navigationController.popViewControllerAnimated(true)
? ? }?else?if?popRecognizer.state?==?UIGestureRecognizerState.Changed?{
? ? ? ??self.interactivePopTransition?.updateInteractiveTransition(progress)
? ? ? ??println("Changed")
? ? }?else?if?popRecognizer.state?==?UIGestureRecognizerState.Ended?|| popRecognizer.state?==?UIGestureRecognizerState.Cancelled?{
? ? ? ??if?progress >?0.5?{
? ? ? ? ? ??self.interactivePopTransition?.finishInteractiveTransition()
? ? ? ? }?else?{
? ? ? ? ? ??self.interactivePopTransition?.cancelInteractiveTransition()
? ? ? ? }
? ? ? ??println("Ended || Cancelled")
? ? ? ??self.interactivePopTransition?=?nil
? ? }
}
~~~
我用了一個實例變量引用UIPercentDrivenInteractiveTransition,這個類只在需要用時才創建,否則在正常Push/Pop的時候,即使只是點擊操作并沒有識別手勢的情況下,也會進入交互(你也可以在要求你返回交互控制器時,進行一些判斷,通過返回nil來屏蔽,但這顯然就太麻煩了)。當手勢識別的時候我們調用pop,用戶手勢發生變化時,調用update去更新,不管是end還是cancel,都判斷下是進入下一個頁面還是返回之前的頁面,完成這一切后把交互控制器清理掉。
現在我們已經有了交互控制器對象,只需要把它給告知給Navigation Controller就行了,我們實現UINavigationControllerDelegate的另一個方法:
~~~
func?navigationController(navigationController:?UINavigationController!, interactionControllerForAnimationController animationController:?UIViewControllerAnimatedTransitioning!) ->?UIViewControllerInteractiveTransitioning! {
? ??return?self.interactivePopTransition
}
~~~
我們從詳情頁面通過自定義的交互動畫返回到上一個頁面的工作就完成了。
Demo效果預覽:

[使用UIPercentDrivenInteractiveTransition的Demo](http://download.csdn.net/download/zhangao0086/7740937)
### 自定義交互控制器
我在之前提過,UIPercentDrivenInteractiveTransition實際上就是實現了**UIViewControllerInteractiveTransitioning**協議,只要是實現了這個協議的對象就可以稱之為交互控制器,我們如果想更加精確的管理動畫以及深入理解處理上的細節,就需要自己實現**UIViewControllerInteractiveTransitioning**協議。
UIViewControllerInteractiveTransitioning協議總共有三個方法,其中startInteractiveTransition:是必須實現的方法,我們在里面初始化動畫的狀態:
~~~
func?startInteractiveTransition(transitionContext:?UIViewControllerContextTransitioning!) {
? ??self.transitionContext = transitionContext
? ??let?containerView = transitionContext.containerView()
? ??let?toViewController = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey)
? ??let?fromViewController = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey)
? ? containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
? ??self.transitingView = fromViewController.view
}
~~~
這里不涉及動畫,只是把需要切換的view添加到上下文環境中即可。動畫部分我們還是和之前使用UIPercentDrivenInteractiveTransition的接口保持一致,添加幾個方法:
~~~
func?updateWithPercent(percent:?CGFloat) {
? ??let?scale =?CGFloat(fabsf(Float(percent?-?CGFloat(1.0))))
? ? transitingView?.transform =?CGAffineTransformMakeScale(scale, scale)
? ? transitionContext?.updateInteractiveTransition(percent)
}
func?finishBy(cancelled:?Bool) {
? ??if?cancelled {
? ? ? ??UIView.animateWithDuration(0.4, animations: {
? ? ? ? ? ??self.transitingView!.transform =?CGAffineTransformIdentity
? ? ? ? ? ? }, completion: {completed?in
? ? ? ? ? ? ? ??self.transitionContext!.cancelInteractiveTransition()
? ? ? ? ? ? ? ??self.transitionContext!.completeTransition(false)
? ? ? ? })
? ? }?else?{
? ? ? ??UIView.animateWithDuration(0.4, animations: {
? ? ? ? ? ? print(self.transitingView)
? ? ? ? ? ??self.transitingView!.transform =?CGAffineTransformMakeScale(0,?0)
? ? ? ? ? ? print(self.transitingView)
? ? ? ? ? ? }, completion: {completed?in
? ? ? ? ? ? ? ??self.transitionContext!.finishInteractiveTransition()
? ? ? ? ? ? ? ??self.transitionContext!.completeTransition(true)
? ? ? ? })
? ? }
}
~~~
updateWithPercent:方法用來更新view的transform屬性,finishBy:方法主要用來判斷是進入下一個頁面還是返回到之前的頁面,并告知transitionContext目前的狀態,以及對當前正在scale的view做最后的動畫。這里的transitionContext和transitingView可以在前面的處理手勢識別代碼中取得,我將里面的代碼更新了一下,變成下面這樣:
~~~
func?handlePopRecognizer(popRecognizer:?UIScreenEdgePanGestureRecognizer) {
? ??var?progress = popRecognizer.translationInView(navigationController.view).x?/?navigationController.view.bounds.size.width
? ? progress =?min(1.0,?max(0.0, progress))
? ??println("\(progress)")
? ??if?popRecognizer.state?==?UIGestureRecognizerState.Began?{
? ? ? ??println("Began")
? ? ? ? isTransiting =?true
? ? ? ??//self.interactivePopTransition = UIPercentDrivenInteractiveTransition()
? ? ? ??self.navigationController.popViewControllerAnimated(true)
? ? }?else?if?popRecognizer.state?==?UIGestureRecognizerState.Changed?{
? ? ? ??//self.interactivePopTransition?.updateInteractiveTransition(progress)
? ? ? ? updateWithPercent(progress)
? ? ? ??println("Changed")
? ? }?else?if?popRecognizer.state?==?UIGestureRecognizerState.Ended?|| popRecognizer.state?==?UIGestureRecognizerState.Cancelled?{
? ? ? ??//if progress > 0.5 {
? ? ? ??//? ? self.interactivePopTransition?.finishInteractiveTransition()
? ? ? ??//} else {
? ? ? ??//? ? self.interactivePopTransition?.cancelInteractiveTransition()
? ? ? ??//}
? ? ? ? finishBy(progress 0.5)
? ? ? ??println("Ended || Cancelled")
? ? ? ? isTransiting =?false
? ? ? ??//self.interactivePopTransition = nil
? ? }
}
~~~
另外還用一個額外布爾值變量isTransiting來標識當前是否在手勢識別中,這是為了在返回交互控制器的時候,不會在不當的時候返回self:
~~~
func?navigationController(navigationController:?UINavigationController!, interactionControllerForAnimationController animationController:
UIViewControllerAnimatedTransitioning!) ->?UIViewControllerInteractiveTransitioning! {
? ??if?!self.isTransiting {
? ? ? ??return?nil
? ? }
? ??return?self
}
~~~
這樣一來就完成了自定義交互控制器。可以發現,基本流程與使用UIPercentDrivenInteractiveTransition是一致的,UIPercentDrivenInteractiveTransition主要是幫我們封裝了transitionContext的初始化以及對它的調用等,只是動畫部分需要我們在額外處理一下了。
[使用自定義交互控制器的Demo](http://download.csdn.net/download/zhangao0086/7741237)
## 最終效果:
我在主頁面上多放了幾個帶Image的Button,在點擊Button時會將Button的Image傳遞到詳情頁面,詳情頁面相應的也有一個UIImageView用來顯示。在主頁面初始化動畫狀態的時候,會生成一個Image的快照來進行動畫,要是在以前,我們只能通過UIGraphics的APIs進行一系列的操作,涉及視圖的scale、旋轉、透明及渲染到context等,但現在,我們只需要用iOS7的API就行了:
~~~
@availability(iOS, introduced=7.0)
func?snapshotViewAfterScreenUpdates(afterUpdates:?Bool) ->?UIView
~~~
這個API能幫助我們快速獲取一個視圖的的快照,afterUpdates參數表示是否等所有效果應用到該視圖之后再獲取,如果設置為false,則立即獲取;為true則會受到后面對該視圖的影響。
在動畫之前,把主頁面和詳情頁面對應的Button和ImageView隱藏,然后對快照生成的View進行動畫,動畫用簡單的frame隱式動畫就可以了。
[最終效果的Demo](https://github.com/zhangao0086/iOS_AnimatedTransition)(上傳到我的資源頁面時總是失敗,所以只能上傳到GitHub上了)
[](https://github.com/zhangao0086/iOS_AnimatedTransition)
最后附上一張圖,這個圖比較容易區分那幾個名稱相近的協議:

## UPDATED:
GitHub上已更新至Xcode 6,主要是語法上的一些小調整
- 前言
- iOS 自定義頁面的切換動畫與交互動畫 By Swift
- Swift 元組(Tuples)介紹
- Swift 可選值(Optional Values)介紹
- Swift Switch介紹
- Swift 值類型和引用類型
- Swift 柯里化(Currying)
- iOS GCD使用指南
- iOS8 Core Image In Swift:自動改善圖像以及內置濾鏡的使用
- 讓Xcode自動更新Build版本
- Swift 全功能的繪圖板開發
- Swift Nullability and Objective-C
- Swift Core Data 圖片存儲與讀取Demo
- Swift 繪圖板功能完善以及終極優化
- 如何設計一個 iOS 控件?(iOS 控件完全解析)