在iOS7之前,開發者為了尋求自定義Navigation Controller的Push/Pop動畫,只能受限於子類化一個UINavigationController,或是用自定義的動畫去覆蓋它。但是隨著iOS7的到來,Apple針對開發者推出了新的工具,以更靈活地方式管理UIViewController切換。
為了在基於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動畫就完成了。
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協議的交互控制器,我們使用它就能夠輕松地為動畫控制器添加一個交互動畫。調用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 }我們從詳情頁面通過自定義的交互動畫返回到上一個頁面的工作就完成了。
使用UIPercentDrivenInteractiveTransition的Demo
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的初始化以及對它的調用等,只是動畫部分需要我們在額外處理一下了。