在iOS7以前,開發人員爲了尋求本身定義Navigation Controller的Push/Pop動畫,僅僅能受限於子類化一個UINavigationController,或是用本身定義的動畫去覆蓋它。但是隨着iOS7的到來,Apple針對開發人員推出了新的工具,以更靈活地方式管理UIViewController切換。git
我把終於的Demo稍作改動,算是找了一個合適的應用場景,另外配上幾張美圖,拉拉人氣。github
儘管是Swift的Demo,但是轉成Objective-C至關easy。ide
爲了在基於UINavigationController下作本身定義的動畫切換,先創建一個簡單的project,這個project的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 = operationspa
return self.net
}code
這是iOS7的新方法,這種方法需要你提供一個UIViewControllerAnimatedTransitioning,那UIViewControllerAnimatedTransitioning到底是什麼呢?orm
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,easy理解;除此以外,還有直接獲取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
一個簡單的本身定義導航欄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)
})
}
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
}
override func prepareForSegue(segue: UIStoryboardSegue!, sender: AnyObject!) {
let modal = segue.destinationViewController as UIViewController
modal.transitioningDelegate = self
}
實際上這個類就是實現了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)
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
}
}
現在咱們已經有了交互控制器對象,僅僅需要把它給告知給Navigation Controller就能夠了,咱們實現UINavigationControllerDelegate的還有一個方法:
func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {
return self.interactivePopTransition
}
咱們從詳情頁面經過本身定義的交互動畫返回到上一個頁面的工做就完畢了。
Demo效果預覽:
使用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
}
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)
})
}
}
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
}
}
func navigationController(navigationController: UINavigationController!, interactionControllerForAnimationController animationController:
UIViewControllerAnimatedTransitioning!) -> UIViewControllerInteractiveTransitioning! {
if !self.isTransiting {
return nil
}
return self
}
@availability(iOS, introduced=7.0)
func snapshotViewAfterScreenUpdates(afterUpdates: Bool) -> UIView