本文翻譯總結自AppCoda以下兩篇文章:
- Introduction to Custom View Controller Transitions and Animations
- Creating a Slide Down Menu Using View Controller Transition
iOS 7開始,蘋果為開發者提供了自定義控制器轉場動畫相關的API,而實現該功能需要以下三個步驟:
- 創建一個類作為動畫管理器,該類需繼承自NSObject并遵守UIViewControllerAnimatedTransitioning協議,我們在這個類中編寫我們的動畫執行代碼。
- 為目標控制器指定轉場動畫代理,既可以使用上一步創建的動畫管理器對象,也可以指定來源控制器作為這個代理。
- 實現代理協議中的相應方法,在方法中返回第一步創建的動畫管理器對象。
準備工作
下載示例程序,地址在這里。(譯注:原文地址需要FQ訪問,本人已轉存到GitHub上,點擊這里。)
示例程序如下圖所示,點擊導航欄上的Action按鈕會modal出一個目標控制器,點擊Dismiss按鈕會返回來源控制器,只不過現在使用的是系統默認的modal動畫,接下來我們就來實現自定義轉場動畫。
創建動畫管理器
創建一個類名稱為CustomPresentAnimationController,繼承自NSObject并遵守UIViewControllerAnimatedTransitioning協議。這個協議有兩個必須實現的方法,我們的實現代碼如下:
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中添加如下代碼:
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效果差不多,不過帶有彈簧效果。如果你希望有不同的效果,你可以對下面這句代碼進行修改。
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, bounds.size.height)
比如將其改為如下代碼:
toViewController.view.frame = CGRectOffset(finalFrameForVC, 0, -bounds.size.height)
再次運行程序,我們的modal動畫就變為從上往下了。
自定義modal過程中退出視圖的動畫
我們的程序現在點擊Dismiss退出目標控制器時,仍然是系統默認的動畫,接下來實現這個自定義動畫。
步驟同前面基本一樣,創建一個叫做CustomDismissAnimationController的動畫管理器,實現如下代理方法:
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中添加如下代碼:
let customDismissAnimationController = CustomDismissAnimationController()
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customDismissAnimationController
}
animationControllerForDismissedController這個代理方法指定了modal過程中退出視圖的動畫。運行程序,你會發現我們的動畫有點小Bug。
我們可以看到,白色的背景視圖確實如我們所愿從中心點逐漸縮小,但是圖片視圖的大小卻保持不變,這是因為改變來源視圖的時候,它的子控件的大小并不會跟著發生改變,我們可以通過視圖快照的技術來解決這一問題。
將animateTransition方法的實現修改為如下代碼:
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協議的方法。這里的動畫代碼采用的是一個開源的三維旋轉動畫,讀者可以到這里自行研究。
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
,然后添加如下代碼:
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,添加如下代碼:
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,添加如下代碼:
let customInteractionController = CustomInteractionController()
然后修改我們之前實現的導航控制器的代理方法如下:
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一個目標控制器時,就為該目標控制器設定交互控制。最后實現導航控制器代理中的另一個方法用于指定交互控制器,代碼如下:
func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
return customInteractionController.transitionInProgress ? customInteractionController : nil
}
運行程序,如下圖所示:

完整的示例程序鏈接地址請點擊這里。
推薦閱讀:
實現下拉菜單的小Demo
Demo實現效果如下圖所示,下載完整的Demo代碼請點擊這里。(譯注:原文地址需要FQ訪問,本人已轉存到GitHub上,點擊這里。)
實現過程同我們前面講的自定義轉場動畫過程一樣,首先創建一個動畫管理器類MenuTransitionManager,然后設置目標控制器的轉場代理,這次我們使用動畫管理器對象作為代理,所以MenuTransitionManager既遵守了UIViewControllerAnimatedTransitioning協議,也遵守了UIViewControllerTransitioningDelegate協議。動畫的執行代碼比較簡單,只是通過改變transform控制來源和目標視圖的上下移動,目標視圖我們仍然使用了快照技術。
我們還為來源視圖的快照添加了一個點擊的手勢,這樣在顯示下拉菜單后,除了點擊相應的菜單選項,點擊下部的快照也可以返回到主頁視圖。只不過點擊手勢的處理我們使用了代理設計模式,而點擊手勢的添加我們使用了Swift的屬性觀察器語法,讀者可以自行研究學習。
最后,希望大家學的愉快!