iOS 7 之後開放了自定義轉場動畫(custom transition view controllers)的方法,從此之後iOS App就變得更有趣了!
我們在切換ViewController的時候,不再是只有上下或左右的推移,而是有了更豐富的轉場效果。
又比如下面這個Dribble上的效果,Jewelry E-commerce Application
關於轉場(Transition)
在iOS 7 之後官方開放了自定義轉場動畫,並且在iOS 10 引進了全程交互控制。
目前官方官方支持這幾種自定義轉場方式:
- UIViewController進行presentation和dismissal的過程(需要modalPresentationStyle為.fullScreen或者custom)
- UINavigationController進行push和pop的過程。
- UICollectionViewController佈局變化時(需要結合UICollectionViewController和UINavigationController的轉場方式)
上面這張圖來自WWDC2013年的一場關於custom viewController transition的介紹。
轉場(Transition)的過程中,作為容器的父ViewController管理者多個子ViewController,但在view的結構上,只會保留一個子ViewController的view。
所以轉場的本質是下一個場景(子ViewController)的View替換掉當前場景(子ViewController以及對應的View)。
這裡指的容器ViewController包括 UINavigationController又或者UITabbarController等等。
轉場代理(Transition Delegate)
需要自定義轉場動畫,第一件事情就是將系統默認的代理改成我們自己的。
- UIViewControllerTransitioningDelegate(UIViewController的transitioningDelegate屬性)
- UINavigationControllerDelegate(UINavigationController的delegate屬性)
- UITabBarControllerDelegate(UITabBarController的delegate屬性)
而我們實現的轉場代理中可以返回動畫控制器(必須)、交互控制器(可選)。
動畫控制器(Animation Controller)
由我們建立一個遵循<UIViewControllerAnimatedTransitioning>協議的類,負責實現轉場動畫的內容。
交互控制器(Interaction Controller)
一個遵循<UIViewControllerInteractiveTransitioning>協議的類,可以讓我們支持使用者通過手勢操作動畫。
轉場上下文(Transition Context)
遵循<UIViewControllerContextTransitioning>,在轉場發生前,系統會給我們所提交的動畫控制器和交互控制器一些信息,比如containerView,是否正在做動畫,動畫是否被取消等。
轉場協調器(Transition Coordinator)
可在執行轉場動畫的同時並行執行其他動畫,它遵循<UIViewControllerTransitionCoordinator>協議,主要用在Modal轉場和交互轉場取消時。
其中Transition Delegate和Animation Controller是實現一個轉場動畫最基本的兩個要素,接下來我們從非交互轉場開始。
非交互轉場
首先需要提供轉場代理和動畫控制器,如果沒有提供系統會使用默認的轉場動畫。
動畫控制器協議<UIViewControllerAnimatedTransitioning>
返回轉場動畫的時間(Required)
1 2 3 |
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.4 } |
具體的動畫實現(Required)
1 2 3 |
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { // 具體的動畫實現 } |
轉場動畫完成後可以在這裡做收尾動作(Optional)
1 2 3 |
func animationEnded(_ transitionCompleted: Bool){ // 動畫完成後的收尾動作 } |
我們在實現轉場動畫的方法中會得到一個遵循UIViewControllerContextTransitioning協議的transitionContext,協議中規範了幾個方法:
參與轉場的ViewController,其中的key包括.to和.from
1 |
func viewController(forKey key: UITransitionContextViewControllerKey) -> UIViewController? |
contrainerView,轉場動畫執行的地方。
1 |
var containerView: UIView { get } |
iOS 8 之後還新增加了viewForKey來直接獲取參與轉場的View,其中的key包括.to和.from
1 |
func view(forKey key: UITransitionContextViewKey) -> UIView? |
從一個場景轉到另外一個場景的過程中
- 對於即將消失的場景我們這裡稱為fromView(fromVC),如上圖ViewControllerA。
- 對於即將出現的場景我們這裡稱為toView(toVC),如上圖ViewControllerB。
通過transitionContext我們可以拿到from (比如上圖的viewControllerA) 或者 to(比如上圖的toViewController) 場景中的內容,來作為轉場動畫的素材。
1 2 3 4 5 |
let containerView = transitionContext.containerView let fromVC = transitionContext.viewController(forKey: .from) let toVC = transitionContext.viewController(forKey: .to) let fromView = transitionContext.view(forKey: .from) let toView = transitionContext.view(forKey: .to) |
動畫控制器
我們這裏做了一個HomeVC跳轉到DetailVC的動畫控制器,在轉場的時候會先將fromView的alpha從1過渡到0,再將toView的alpha從0過渡到1。
把fromView和toView放入containerView中,然後在處理漸變的動畫。
在轉場動畫完成的時候執行transitionContext.completeTransition()來通知系統轉場結束了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class SKSimpleTransition:NSObject, UIViewControllerAnimatedTransitioning { // 定義轉場動畫為0.8秒 func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { return 0.8 } // 具體的轉場動畫 func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { let fromVC = transitionContext.viewController(forKey: .from) let fromView = fromVC?.view let toVC = transitionContext.viewController(forKey: .to) let toView = toVC?.view let containerView = transitionContext.containerView containerView.addSubview(fromView!) containerView.addSubview(toView!) // 轉場動畫 toView?.alpha = 0 UIView.animate(withDuration: 0.4, animations: { fromView?.alpha = 0 }, completion: { finished in UIView.animate(withDuration: 0.4, animations: { toView?.alpha = 1 }, completion: { finished in // 通知完成轉場 transitionContext.completeTransition(true) }) }) } } |
轉場代理
- UINavigationControllerDelegate
- UITabBarControllerDelegate
- UIViewControllerTransitioningDelegate
UINavigationControllerDelegate
如果上面的例子,我們在homeVC中,將UINavigationControllerDelegate設定為homeVC本身
1 |
navigationController?.delegate = self |
我們告訴系統,detailVC的轉場動畫由我們自己來實現。
1 2 3 4 |
let detailVC = DetailViewController(nibName: "DetailViewController", bundle: nil) detailVC.modalPresentationStyle = .custom detailVC.modalTransitionStyle = .crossDissolve navigationController?.pushViewController(detailVC, animated: true) |
接著在UINavigationControllerDelegate的方法中提供動畫控制器。
1 2 3 4 5 6 7 |
// MARK: UINavigationControllerDelegate extension HomeViewController: UINavigationControllerDelegate { func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { let transition = SKSimpleTransition() return transition } } |
UITabBarControllerDelegate
1 2 3 4 5 6 7 8 |
// MARK: UITabBarControllerDelegate extension HomeViewController: UITabBarControllerDelegate { func tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { let transition = SKSimpleTransition() return transition } } |
特殊的Modal轉場
UIViewControllerTransitioningDelegate
對於Presentations 和 Dismissals目前支持兩種形式
- UIModalPresentationFullScreen
- UIModalPresentationCustom
使用起來也是非常容易的,設定modalPresentationStyle,給予一個transitioningDelegate。
1 2 3 4 |
let detailVC = DetailViewController(nibName: "DetailViewController", bundle: nil) detailVC.modalPresentationStyle = .custom detailVC.transitioningDelegate = self present(detailVC, animated: true, completion: nil) |
實現UIViewControllerTransitioningDelegate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// MARK: UIViewControllerTransitioningDelegate extension HomeViewController: UIViewControllerTransitioningDelegate { func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { let transition = SKSimpleTransition() return transition } func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { let transition = SKSimpleTransition() return transition } } |
需要注意的是:
- FullScreen模式下,在Presentation結束後,PresentingView會被移出View結構,但因為系統會幫忙處理還原的部分,
所以操作起來和前面容器類的ViewController轉場操作起來是一樣的。 - Custom模式下,在Presentation結束後,PresentingView(fromView)未被移出View結構,所以如果在present或者dismiss的
動畫控制器中將homeViewController的view加入到了container中,在轉場結束後就會跟著container一起被移除而出現黑屏的現象。
即在Custom模式下進行dismiss轉場動畫,如果把toView加入containerView會造成toView消失,而FullScreen模式不會。
presentingView被移除出現黑屏的例子
舉例一個自定義Modal轉場動畫流程的例子
- 設定轉場代理(setTransitioningDelegate)
- 執行present動作
- 提供動畫控制器(animationController)
動畫控制器中
- 提供一個遵從轉場動畫的執行時間(transitionDuration)
- 具體的動畫執行過程(animateTransition)
- 動畫完成時,執行completeTransition()
更多閱讀:
- WWDC: Custom Transitions using View Controllers
- iOS視圖控制器轉場詳解
- 可以到Github上看本文提到的三種轉場例子。