iOS UIViewControllerTransitioning 自定義轉(zhuǎn)場框架的使用

iOS UIViewControllerTransitioning 自定義轉(zhuǎn)場框架的使用

iOS UIViewControllerTransitioning 自定義轉(zhuǎn)場框架的使用 1

1 簡介 1

2 轉(zhuǎn)場協(xié)議 2

2.1轉(zhuǎn)場代理(Transition Delegate): 2

2.2.動畫控制器(Animation Controller): 2

2.3.交互控制器(Interaction Controller): 3

2.4.轉(zhuǎn)場環(huán)境(Transition Context): 3

2.5.轉(zhuǎn)場協(xié)調(diào)器(Transition Coordinator): 3

3 非交互式轉(zhuǎn)場 3

3.1 動畫控制器協(xié)議 3

3.2 動畫控制器實現(xiàn) 5

3.3 特殊的 Modal 轉(zhuǎn)場 6

4 交互式轉(zhuǎn)場 10

Transition Coordinator 13

交互轉(zhuǎn)場的限制 14

5 UICollectionViewController 布局轉(zhuǎn)場 15

1 簡介

在 iOS 7 之前,我們只能使用系統(tǒng)提供的轉(zhuǎn)場效果,大部分時候夠用,但僅僅是夠用而已,總歸會有各種不如意的小地方,但我們卻無力改變;iOS 7 開放了相關(guān) API 允許我們對轉(zhuǎn)場效果進行全面定制,目前為止,官方支持以下幾種方式的自定義轉(zhuǎn)場:

(1)、在 UINavigationController 中 push 和 pop;

(2)、在 UITabBarController 中切換 Tab;

(3)、Modal 轉(zhuǎn)場:presentation 和 dismiss,俗稱視圖控制器的模態(tài)顯示和消失,僅限于modalPresentationStyle屬性為 UIModalPresentationFullScreen 或 UIModalPresentationCustom 這兩種模式;

(4)、UICollectionViewController 的布局轉(zhuǎn)場;

2 轉(zhuǎn)場協(xié)議

iOS 7 以協(xié)議的方式開放了自定義轉(zhuǎn)場的 API,協(xié)議的好處是不再拘泥于具體的某個類,只要是遵守該協(xié)議的對象都能參與轉(zhuǎn)場,非常靈活。轉(zhuǎn)場協(xié)議由5種協(xié)議組成,在實際中只需要我們提供其中的兩個或三個便能實現(xiàn)絕大部分的轉(zhuǎn)場動畫

2.1轉(zhuǎn)場代理(Transition Delegate):

自定義轉(zhuǎn)場的第一步便是提供轉(zhuǎn)場代理,告訴系統(tǒng)使用我們提供的代理而不是系統(tǒng)的默認代理來執(zhí)行轉(zhuǎn)場。有如下三種轉(zhuǎn)場代理,對應(yīng)上面三種類型的轉(zhuǎn)場:

//1. UINavigationController 的 delegate 遵循的協(xié)議

UINavigationControllerDelegate。

//2. UITabBarController 的 delegate 遵循的協(xié)議。

UITabBarControllerDelegate

// 3.UIViewController 的 transitioningDelegate 遵循的協(xié)議。

UIViewControllerTransitioningDelegate

這里除了<UIViewControllerTransitioningDelegate>是 iOS 7 新增的協(xié)議,其他兩種在 iOS 2 里就存在了,在 iOS 7 時擴充了這種協(xié)議來支持自定義轉(zhuǎn)場。

轉(zhuǎn)場發(fā)生時,UIKit 將要求轉(zhuǎn)場代理將提供轉(zhuǎn)場動畫的核心構(gòu)件:動畫控制器和交互控制器(可選的)由我們實現(xiàn)。

2.2.動畫控制器(Animation Controller):

最重要的部分,負責添加視圖以及執(zhí)行動畫;遵守<UIViewControllerAnimatedTransitioning>協(xié)議;由我們實現(xiàn)。

2.3.交互控制器(Interaction Controller):

通過交互手段,通常是手勢來驅(qū)動動畫控制器實現(xiàn)的動畫,使得用戶能夠控制整個過程;遵守<UIViewControllerInteractiveTransitioning>協(xié)議;系統(tǒng)已經(jīng)打包好現(xiàn)成的類供我們使用。

2.4.轉(zhuǎn)場環(huán)境(Transition Context):

提供轉(zhuǎn)場中需要的數(shù)據(jù);遵守<UIViewControllerContextTransitioning>協(xié)議;由 UIKit 在轉(zhuǎn)場開始前生成并提供給我們提交的動畫控制器和交互控制器使用。

2.5.轉(zhuǎn)場協(xié)調(diào)器(Transition Coordinator):

可在轉(zhuǎn)場動畫發(fā)生的同時并行執(zhí)行其他的動畫,其作用與其說協(xié)調(diào)不如說輔助,主要在 Modal 轉(zhuǎn)場和交互轉(zhuǎn)場取消時使用,其他時候很少用到;遵守<UIViewControllerTransitionCoordinator>協(xié)議;由 UIKit 在轉(zhuǎn)場時生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一個遵守該協(xié)議的對象,且該方法只在該控制器處于轉(zhuǎn)場過程中才返回一個此類對象,不參與轉(zhuǎn)場時返回 nil。

3 非交互式轉(zhuǎn)場

這個階段要做兩件事,提供轉(zhuǎn)場代理并由代理提供動畫控制器。在轉(zhuǎn)場代理協(xié)議里動畫控制器和交互控制器都是可選實現(xiàn)的,沒有實現(xiàn)或者返回 nil 的話則使用默認的轉(zhuǎn)場效果。

3.1 動畫控制器協(xié)議

動畫控制器負責添加視圖以及執(zhí)行動畫,遵守UIViewControllerAnimatedTransitioning協(xié)議,該協(xié)議要求實現(xiàn)以下方法:

//執(zhí)行動畫的地方,最核心的方法。

(Required)func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)

//返回動畫時間,"return 0.5" 已足夠,非常簡單

(Required)func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval

//如果實現(xiàn)了,會在轉(zhuǎn)場動畫結(jié)束后調(diào)用,可以執(zhí)行一些收尾工作。

(Optional)func animationEnded(_ transitionCompleted: Bool)

最重要的是第一個方法,該方法接受一個遵守<UIViewControllerContextTransitioning>協(xié)議的轉(zhuǎn)場環(huán)境對象,上一節(jié)的 API 解釋里提到這個協(xié)議,它提供了轉(zhuǎn)場所需要的重要數(shù)據(jù):參與轉(zhuǎn)場的視圖控制器和轉(zhuǎn)場過程的狀態(tài)信息。

UIKit 在轉(zhuǎn)場開始前生成遵守轉(zhuǎn)場環(huán)境協(xié)議<UIViewControllerContextTransitioning>的對象 transitionContext,它有以下幾個方法來提供動畫控制器需要的信息:

//返回容器視圖,轉(zhuǎn)場動畫發(fā)生的地方。 func containerView() -> UIView?

//獲取參與轉(zhuǎn)場的視圖控制器,有 UITransitionContextFromViewControllerKey 和 UITransitionContextToViewControllerKey 兩個 Key。

func viewControllerForKey(_ key: String) -> UIViewController? //iOS 8新增 API 用于方便獲取參與參與轉(zhuǎn)場的視圖,有 UITransitionContextFromViewKey 和 UITransitionContextToViewKey 兩個 Key。

func viewForKey(_ key: String) -> UIView? AVAILABLE_IOS(8_0)

通過viewForKey:獲取的視圖是viewControllerForKey:返回的控制器的根視圖,或者 nil。viewForKey:方法返回 nil 只有一種情況: UIModalPresentationCustom 模式下的 Modal 轉(zhuǎn)場 ,通過此方法獲取 presentingView 時得到的將是 nil,在后面的 Modal 轉(zhuǎn)場里會詳細解釋。

前面提到轉(zhuǎn)場的本質(zhì)是下一個場景的視圖替換當前場景的視圖,從當前場景過渡下一個場景。下面稱即將消失的場景的視圖為 fromView,對應(yīng)的視圖控制器為 fromVC,即將出現(xiàn)的視圖為 toView,對應(yīng)的視圖控制器稱之為 toVC。幾種轉(zhuǎn)場方式的轉(zhuǎn)場操作都是可逆的,一種操作里的 fromView 和 toView 在逆向操作里的角色互換成對方,fromVC 和 toVC 也是如此。 在動畫控制器里,參與轉(zhuǎn)場的視圖只有 fromView toView 之分,與轉(zhuǎn)場方式無關(guān)。轉(zhuǎn)場動畫的最終效果只限制于你的想象力。 這也是動畫控制器在封裝后可以被第三方使用的重要原因。

在 iOS 8 中可通過以下方法來獲取參與轉(zhuǎn)場的三個重要視圖,在 iOS 7 中則需要通過對應(yīng)的視圖控制器來獲取,為避免 API 差異導致代碼過長,示例代碼中直接使用下面的視圖變量:

let containerView = transitionContext.containerView()

let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)

let toView = transitionContext.viewForKey(UITransitionContextToViewKey)

3.2 動畫控制器實現(xiàn)

轉(zhuǎn)場 API 是協(xié)議的好處是不限制具體的類,只要對象實現(xiàn)該協(xié)議便能參與轉(zhuǎn)場過程,這也帶來另外一個好處:封裝便于復用,盡管三大轉(zhuǎn)場代理協(xié)議的方法不盡相同,但它們返回的動畫控制器遵守的是同一個協(xié)議,因此可以將動畫控制器封裝作為第三方動畫控制器在其他控制器的轉(zhuǎn)場過程中使用。

func animateTransition(transitionContext: UIViewControllerContextTransitioning) { ...

//1

containerView.addSubview(toView)

//計算位移 transform,NavigationVC 和 TabBarVC 在水平方向進行動畫,Modal 轉(zhuǎn)場在豎直方向進行動畫。

var toViewTransform = ...

var fromViewTransform = ...

toView.transform = toViewTransform

//根據(jù)協(xié)議中的方法獲取動畫的時間。

let duration = self.transitionDuration(transitionContext)

UIView.animateWithDuration(duration, animations: {

fromView.transform = fromViewTransform

toView.transform = CGAffineTransformIdentity

}, completion: { _ in

//考慮到轉(zhuǎn)場中途可能取消的情況,轉(zhuǎn)場結(jié)束后,恢復視圖狀態(tài)。

fromView.transform = CGAffineTransformIdentity

toView.transform = CGAffineTransformIdentity

//2

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

}

注意上面的代碼有2處標記,是動畫控制器必須完成的:

  1. 將 toView 添加到容器視圖中,使得 toView 在屏幕上顯示( Modal 轉(zhuǎn)場中此點稍有不同,下一節(jié)細述);
  2. 正確地結(jié)束轉(zhuǎn)場過程。轉(zhuǎn)場的結(jié)果有兩種:完成或取消。非交互轉(zhuǎn)場的結(jié)果只有完成一種情況,不過交互式轉(zhuǎn)場需要考慮取消的情況。如何結(jié)束取決于轉(zhuǎn)場的進度,通過transitionWasCancelled()方法來獲取轉(zhuǎn)場的狀態(tài),使用completeTransition:來完成或取消轉(zhuǎn)場。

3.3 特殊的 Modal 轉(zhuǎn)場

UINavigationController 和 UITabBarController 這兩個容器 VC 的根視圖在屏幕上是不可見的(或者說是透明的),可見的只是內(nèi)嵌在這兩者中的子 VC 中的視圖,轉(zhuǎn)場是從子 VC 的視圖轉(zhuǎn)換到另外一個子 VC 的視圖,其根視圖并未參與轉(zhuǎn)場;而 Modal 轉(zhuǎn)場,以 presentation 為例,是從 presentingView 轉(zhuǎn)換到 presentedView,根視圖 presentingView 也就是 fromView 參與了轉(zhuǎn)場。而且 NavigationController 和 TabBarController 轉(zhuǎn)場中的 containerView 也并非這兩者的根視圖。

[圖片上傳失敗...(image-d2e88f-1616030414999)]

Modal 轉(zhuǎn)場與兩種容器 VC 的轉(zhuǎn)場的另外一個不同是:Modal 轉(zhuǎn)場結(jié)束后 presentingView 可能依然可見,UIModalPresentationPageSheet 模式就是這樣。這種不同導致了 Modal 轉(zhuǎn)場和容器 VC 的轉(zhuǎn)場對 fromView 的處理差異:容器 VC 的轉(zhuǎn)場結(jié)束后 fromView 會被主動移出視圖結(jié)構(gòu),這是可預見的結(jié)果,我們也可以在轉(zhuǎn)場結(jié)束前手動移除;而 Modal 轉(zhuǎn)場中,presentation 結(jié)束后 presentingView(fromView) 并未主動被從視圖結(jié)構(gòu)中移除。準確來說,是 UIModalPresentationCustom 這種模式下的 Modal 轉(zhuǎn)場結(jié)束時 fromView 并未從視圖結(jié)構(gòu)中移除;UIModalPresentationFullScreen 模式的 Modal 轉(zhuǎn)場結(jié)束后 fromView 依然主動被從視圖結(jié)構(gòu)中移除了。這種差異導致在處理 dismissal 轉(zhuǎn)場的時候很容易出現(xiàn)問題,沒有意識到這個不同點的話出錯時就會毫無頭緒。下面來看看 dismissal 轉(zhuǎn)場時的場景。

ContainerView 在轉(zhuǎn)場期間作為 fromView 和 toView 的父視圖。三種轉(zhuǎn)場過程中的 containerView 是 UIView 的私有子類,不過我們并不需要關(guān)心 containerView 具體是什么。在 dismissal 轉(zhuǎn)場中:

  1. UIModalPresentationFullScreen 模式:presentation 后,presentingView 被主動移出視圖結(jié)構(gòu),在 dismissal 中 presentingView 是 toView 的角色,其將會重新加入 containerView 中,實際上,我們不主動將其加入,UIKit 也會這么做,前面的兩種容器控制器的轉(zhuǎn)場里不是這樣處理的,不過這個差異基本沒什么影響。
  2. UIModalPresentationCustom 模式:轉(zhuǎn)場時 containerView 并不擔任 presentingView 的父視圖,后者由 UIKit 另行管理。在 presentation 后,fromView(presentingView) 未被移出視圖結(jié)構(gòu),在 dismissal 中,注意不要像其他轉(zhuǎn)場中那樣將 toView(presentingView) 加入 containerView 中,否則本來可見的 presentingView 將會被移除出自身所處的視圖結(jié)構(gòu)消失不見。如果你在使用 Custom 模式時沒有注意到這點,就很容易掉進這個陷阱而很難察覺問題所在

對于 Custom 模式,我們可以參照其他轉(zhuǎn)場里的處理規(guī)則來打理:presentation 轉(zhuǎn)場結(jié)束后主動將 fromView(presentingView) 移出它的視圖結(jié)構(gòu),并用一個變量來維護 presentingView 的父視圖,以便在 dismissal 轉(zhuǎn)場中恢復;在 dismissal 轉(zhuǎn)場中,presentingView 的角色由原來的 fromView 切換成了 toView,我們再將其重新恢復它原來的視圖結(jié)構(gòu)中。測試表明這樣做是可行的。但是這樣一來,在實現(xiàn)上,需要在轉(zhuǎn)場代理中維護一個動畫控制器并且這個動畫控制器要維護 presentingView 的父視圖,第三方的動畫控制器必須為此改造。顯然,這樣的代價是無法接受的。

小結(jié) :經(jīng)過上面的嘗試,建議是,不要干涉官方對 Modal 轉(zhuǎn)場的處理,我們?nèi)ミm應(yīng)它。在 Custom 模式下,由于 presentingView 不受 containerView 管理,在 dismissal 轉(zhuǎn)場中不要像其他的轉(zhuǎn)場那樣將 toView(presentingView) 加入 containerView,否則 presentingView 將消失不見,而應(yīng)用則也很可能假死;而在 presentation 轉(zhuǎn)場中,切記不要手動將 fromView(presentingView) 移出其父視圖。

iOS 8 為<UIViewControllerContextTransitioning>協(xié)議添加了viewForKey:方法以方便獲取 fromView 和 toView,但是在 Modal 轉(zhuǎn)場里要注意,從上面可以知道,Custom 模式下,presentingView 并不受 containerView 管理,這時通過viewForKey:方法來獲取 presentingView 得到的是 nil,必須通過viewControllerForKey:得到 presentingVC 后來獲取。因此在 Modal 轉(zhuǎn)場中,較穩(wěn)妥的方法是從 fromVC 和 toVC 中獲取 fromView 和 toView。

順帶一提,前面提到的UIView的類方法transitionFromView:toView:duration:options:completion:能在 Custom 模式下工作,卻與 FullScreen 模式有點不兼容。

Model 轉(zhuǎn)場實現(xiàn):

func animateTransition(transitionContext: UIViewControllerContextTransitioning) {

...

//不像容器 VC 轉(zhuǎn)場里需要額外的變量來標記操作類型,UIViewController 自身就有方法跟蹤 Modal 狀態(tài)。

//處理 Presentation 轉(zhuǎn)場:

if toVC.isBeingPresented(){

//1

containerView.addSubview(toView)

//在 presentedView 后面添加暗背景視圖 dimmingView,注意兩者在 containerView 中的位置。

let dimmingView = UIView()

containerView.insertSubview(dimmingView, belowSubview: toView)

//設(shè)置 presentedView 和 暗背景視圖 dimmingView 的初始位置和尺寸。

let toViewWidth = containerView.frame.width * 2 / 3

let toViewHeight = containerView.frame.height * 2 / 3

toView.center = containerView.center

toView.bounds = CGRect(x: 0, y: 0, width: 1, height: toViewHeight)

dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)

dimmingView.center = containerView.center

dimmingView.bounds = CGRect(x: 0, y: 0, width: toViewWidth, height: toViewHeight)

//實現(xiàn)出現(xiàn)時的尺寸變化的動畫:

UIView.animateWithDuration(duration, delay: 0, options: .CurveEaseInOut, animations: {

toView.bounds = CGRect(x: 0, y: 0, width: toViewWidth, height: toViewHeight)

dimmingView.bounds = containerView.bounds

}, completion: {_ in

//2

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

}

//處理 Dismissal 轉(zhuǎn)場,按照上一小節(jié)的結(jié)論,.Custom 模式下不要將 toView 添加到 containerView,省去了上面標記1處的操作。

if fromVC.isBeingDismissed(){

let fromViewHeight = fromView.frame.height

UIView.animateWithDuration(duration, animations:

{

fromView.bounds = CGRect(x: 0, y: 0, width: 1, height: fromViewHeight)

}, completion: { _ in

//2

let isCancelled = transitionContext.transitionWasCancelled()

transitionContext.completeTransition(!isCancelled)

})

} }

4 交互式轉(zhuǎn)場

在非交互轉(zhuǎn)場的基礎(chǔ)上將之交互化需要兩個條件:

  1. 由轉(zhuǎn)場代理提供交互控制器,這是一個遵守<UIViewControllerInteractiveTransitioning>協(xié)議的對象,不過系統(tǒng)已經(jīng)打包好了現(xiàn)成的類UIPercentDrivenInteractiveTransition供我們使用。我們不需要做任何配置,僅僅在轉(zhuǎn)場代理的相應(yīng)方法中提供一個該類實例便能工作。另外交互控制器必須有動畫控制器才能工作。
  2. 交互控制器還需要交互手段的配合,最常見的是使用手勢,或是其他事件,來驅(qū)動整個轉(zhuǎn)場進程。

滿足以上兩個條件很簡單,但是很容易犯錯誤。

正確地提供交互控制器

如果在轉(zhuǎn)場代理中提供了交互控制器,而轉(zhuǎn)場發(fā)生時并沒有方法來驅(qū)動轉(zhuǎn)場進程(比如手勢),轉(zhuǎn)場過程將一直處于開始階段無法結(jié)束,應(yīng)用界面也會失去響應(yīng):在 NavigationController 中點擊 NavigationBar 也能實現(xiàn) pop 返回操作,但此時沒有了交互手段的支持,轉(zhuǎn)場過程卡殼;在 TabBarController 的代理里提供交互控制器存在同樣的問題,點擊 TabBar 切換頁面時也沒有實現(xiàn)交互控制。因此僅在確實處于交互狀態(tài)時才提供交互控制器,可以使用一個變量來標記交互狀態(tài),該變量由交互手勢來更新狀態(tài)。

以為 NavigationController 提供交互控制器為例:

class SDENavigationDelegate: NSObject, UINavigationControllerDelegate { var interactive = false let interactionController = UIPercentDrivenInteractiveTransition() ... func navigationController(navigationController: UINavigationController, interactionControllerForAnimationController animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { return interactive ? self.interactionController : nil } }

TabBarController 的實現(xiàn)類似,Modal 轉(zhuǎn)場代理分別為 presentation 和 dismissal 提供了各自的交互控制器,也需要注意上面的問題。

問題的根源是交互控制的工作機制導致的,交互過程實際上是由轉(zhuǎn)場環(huán)境對象<UIViewControllerContextTransitioning>來管理的,它提供了如下幾個方法來控制轉(zhuǎn)場的進度:

func updateInteractiveTransition(_ percentComplete: CGFloat)//更新轉(zhuǎn)場進度,進度數(shù)值范圍為0.0~1.0。 func cancelInteractiveTransition()//取消轉(zhuǎn)場,轉(zhuǎn)場動畫從當前狀態(tài)返回至轉(zhuǎn)場發(fā)生前的狀態(tài)。 func finishInteractiveTransition()//完成轉(zhuǎn)場,轉(zhuǎn)場動畫從當前狀態(tài)繼續(xù)直至結(jié)束。

交互控制協(xié)議<UIViewControllerInteractiveTransitioning>只有一個必須實現(xiàn)的方法:

func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning)

在轉(zhuǎn)場代理里提供了交互控制器后,轉(zhuǎn)場開始時,該方法自動被 UIKit 調(diào)用對轉(zhuǎn)場環(huán)境進行配置。

系統(tǒng)打包好的UIPercentDrivenInteractiveTransition中的控制轉(zhuǎn)場進度的方法與轉(zhuǎn)場環(huán)境對象提供的三個方法同名,實際上只是前者調(diào)用了后者的方法而已。系統(tǒng)以一種解耦的方式使得動畫控制器,交互控制器,轉(zhuǎn)場環(huán)境對象互相協(xié)作,我們只需要使用UIPercentDrivenInteractiveTransition的三個同名方法來控制進度就夠了。如果你要實現(xiàn)自己的交互控制器,而不是UIPercentDrivenInteractiveTransition的子類,就需要調(diào)用轉(zhuǎn)場環(huán)境的三個方法來控制進度,壓軸環(huán)節(jié)我們將示范如何做。

交互控制器控制轉(zhuǎn)場的過程就像將動畫控制器實現(xiàn)的動畫制作成一部視頻,我們使用手勢或是其他方法來控制轉(zhuǎn)場動畫的播放,可以前進,后退,繼續(xù)或者停止。finishInteractiveTransition()方法被調(diào)用后,轉(zhuǎn)場動畫從當前的狀態(tài)將繼續(xù)進行直到動畫結(jié)束,轉(zhuǎn)場完成;cancelInteractiveTransition()被調(diào)用后,轉(zhuǎn)場動畫從當前的狀態(tài)回撥到初始狀態(tài),轉(zhuǎn)場取消。

在 NavigationController 中點擊 NavigationBar 的 backBarButtomItem 執(zhí)行 pop 操作時,由于我們無法介入 backBarButtomItem 的內(nèi)部流程,就失去控制進度的手段,于是轉(zhuǎn)場過程只有一個開始,永遠不會結(jié)束。其實我們只需要有能夠執(zhí)行上述幾個方法的手段就可以對轉(zhuǎn)場動畫進行控制,用戶與屏幕的交互手段里,手勢是實現(xiàn)這個控制過程的天然手段,我猜這是其被稱為交互控制器的原因。

交互手段的配合

下面使用演示如何利用屏幕邊緣滑動手勢UIScreenEdgePanGestureRecognizer在 NavigationController 中控制 Slide 動畫控制器提供的動畫來實現(xiàn)右滑返回的效果,該手勢綁定的動作方法如下:

func handleEdgePanGesture(gesture: UIScreenEdgePanGestureRecognizer){ //根據(jù)移動距離計算交互過程的進度。 let percent = ... switch gesture.state{ case .Began: //轉(zhuǎn)場開始前獲取代理,一旦轉(zhuǎn)場開始,VC 將脫離控制器棧,此后 self.navigationController 返回的是 nil。 self.navigationDelegate = self.navigationController?.delegate as? SDENavigationDelegate //更新交互狀態(tài) self.navigationDelegate?.interactive = true //1.交互控制器沒有 start 之類的方法,當下面這行代碼執(zhí)行后,轉(zhuǎn)場開始; //如果轉(zhuǎn)場代理提供了交互控制器,它將從這時候開始接管轉(zhuǎn)場過程。 self.navigationController?.popViewControllerAnimated(true) case .Changed: //2.更新進度: self.navigationDelegate?.interactionController.updateInteractiveTransition(percent) case .Cancelled, .Ended: //3.結(jié)束轉(zhuǎn)場: if percent > 0.5{ //完成轉(zhuǎn)場。 self.navigationDelegate?.interactionController.finishInteractiveTransition() }else{ //或者,取消轉(zhuǎn)場。 self.navigationDelegate?.interactionController.cancelInteractiveTransition() } //無論轉(zhuǎn)場的結(jié)果如何,恢復為非交互狀態(tài)。 self.navigationDelegate?.interactive = false default: self.navigationDelegate?.interactive = false } }

交互轉(zhuǎn)場的流程就是三處數(shù)字標記的代碼。不管是什么交互方式,使用什么轉(zhuǎn)場方式,都是在使用這三個方法控制轉(zhuǎn)場的進度。 對于交互式轉(zhuǎn)場,交互手段只是表現(xiàn)形式,本質(zhì)是驅(qū)動轉(zhuǎn)場進程。 很希望能夠看到更新穎的交互手法,比如通過點擊頁面不同區(qū)域來控制一套復雜的流程動畫。TabBarController 的 Demo 中也實現(xiàn)了滑動切換 Tab 頁面,代碼是類似的,就不占篇幅了;示范的 Modal 轉(zhuǎn)場我沒有為之實現(xiàn)交互控制,原因也提到過了,沒有比較合乎操作直覺的交互手段,不過真要為其添加交互控制,代碼和上面是類似的。

轉(zhuǎn)場交互化后結(jié)果有兩種:完成和取消。取消后動畫將會原路返回到初始狀態(tài),但已經(jīng)變化了的數(shù)據(jù)怎么恢復?

一種情況是,控制器的系統(tǒng)屬性,比如,在 TabBarController 里使用上面的方法實現(xiàn)滑動切換 Tab 頁面,中途取消的話,已經(jīng)變化的selectedIndex屬性該怎么恢復為原值;上面的代碼里,取消轉(zhuǎn)場的代碼執(zhí)行后,self.navigationController返回的依然還是是 nil,怎么讓控制器回到 NavigationController 的控制器棧頂。對于這種情況,UIKit 自動替我們恢復了,不需要我們操心(可能你都沒有意識到這回事);

另外一種就是,轉(zhuǎn)場發(fā)生的過程中,你可能想實現(xiàn)某些效果,一般是在下面的事件中執(zhí)行,轉(zhuǎn)場中途取消的話可能需要取消這些效果。

func viewWillAppear(_ animated: Bool) func viewDidAppear(_ animated: Bool) func viewWillDisappear(_ animated: Bool) func viewDidDisappear(_ animated: Bool)

交互轉(zhuǎn)場介入后,視圖在這些狀態(tài)間的轉(zhuǎn)換變得復雜,WWDC 上蘋果的工程師還表示轉(zhuǎn)場過程中 view 的Will系方法和Did系方法的執(zhí)行順序并不能得到保證,雖然幾率很小,但如果你依賴于這些方法執(zhí)行的順序的話就可能需要注意這點。而且,Did系方法調(diào)用時并不意味著轉(zhuǎn)場過程真的結(jié)束了。另外,fromView 和 toView 之間的這幾種方法的相對順序更加混亂,具體的案例可以參考這里:The Inconsistent Order of View Transition Events

如何在轉(zhuǎn)場過程中的任意階段中斷時取消不需要的效果?這時候該轉(zhuǎn)場協(xié)調(diào)器(Transition Coordinator)再次出場了。

Transition Coordinator

轉(zhuǎn)場協(xié)調(diào)器(Transition Coordinator)的出場機會不多,但卻是關(guān)鍵先生。Modal
轉(zhuǎn)場中,UIPresentationController類只能通過轉(zhuǎn)場協(xié)調(diào)器來與動畫控制器同步,并行執(zhí)行其他動畫;這里它可以在交互式轉(zhuǎn)場結(jié)束時執(zhí)行一個閉包:

func notifyWhenInteractionEndsUsingBlock(_ handler: (UIViewControllerTransitionCoordinatorContext) -> Void)

當轉(zhuǎn)場由交互狀態(tài)轉(zhuǎn)變?yōu)榉墙换顟B(tài)(在手勢交互過程中則為手勢結(jié)束時),無論轉(zhuǎn)場的結(jié)果是完成還是被取消,該方法都會被調(diào)用;得益于閉包,轉(zhuǎn)場協(xié)調(diào)器可以在轉(zhuǎn)場過程中的任意階段搜集動作并在交互中止后執(zhí)行。閉包中的參數(shù)是一個遵守<UIViewControllerTransitionCoordinatorContext>協(xié)議的對象,該對象由 UIKit 提供,和前面的轉(zhuǎn)場環(huán)境對象<UIViewControllerContextTransitioning>作用類似,它提供了交互轉(zhuǎn)場的狀態(tài)信息。

override func viewWillAppear(animated: Bool) { super.viewWillDisappear(animated) self.doSomeSideEffectsAssumingViewDidAppearIsGoingToBeCalled() //只在處于交互轉(zhuǎn)場過程中才可能取消效果。 if let coordinator = self.transitionCoordinator() where coordinator.initiallyInteractive() == true{ coordinator.notifyWhenInteractionEndsUsingBlock({ interactionContext in if interactionContext.isCancelled(){ self.undoSideEffects() } }) } }

不過交互狀態(tài)結(jié)束時并非轉(zhuǎn)場過程的終點(此后動畫控制器提供的轉(zhuǎn)場動畫根據(jù)交互結(jié)束時的狀態(tài)繼續(xù)或是返回到初始狀態(tài)),而是由動畫控制器來結(jié)束這一切:

optional func animationEnded(_ transitionCompleted: Bool)

如果實現(xiàn)了該方法,將在轉(zhuǎn)場動畫結(jié)束后調(diào)用。

UIViewController 可以通過transitionCoordinator()獲取轉(zhuǎn)場協(xié)調(diào)器,該方法的文檔中說只有在 Modal 轉(zhuǎn)場過程中,該方法才返回一個與當前轉(zhuǎn)場相關(guān)的有效對象。實際上,NavigationController 的轉(zhuǎn)場中 fromVC 和 toVC 也能返回一個有效對象,TabBarController 有點特殊,fromVC 和 toVC 在轉(zhuǎn)場中返回的是 nil,但是作為容器的 TabBarController 可以使用該方法返回一個有效對象。

轉(zhuǎn)場協(xié)調(diào)器除了上面的兩種關(guān)鍵作用外,也在 iOS 8 中的適應(yīng)性布局中擔任重要角色,可以查看<UIContentContainer>協(xié)議中的方法,其中響應(yīng)尺寸和屏幕旋轉(zhuǎn)事件的方法都包含一個轉(zhuǎn)場協(xié)調(diào)器對象,視圖的這種變化也被系統(tǒng)視為廣義上的 transition,參數(shù)中的轉(zhuǎn)場協(xié)調(diào)器也由 UIKit 提供。這個話題有點超出本文的范圍,就不深入了,有需要的話可以查看文檔和相關(guān) session。

交互轉(zhuǎn)場的限制

如果希望轉(zhuǎn)場中的動畫能完美地被交互控制,必須滿足2個隱性條件:

  1. 使用 UIView 動畫的 API。你當然也可以使用 Core Animation 來實現(xiàn)動畫,甚至,這種動畫可以被交互控制,但是當交互中止時,會出現(xiàn)一些意外情況:如果你正確地用 Core Animation 的方式復現(xiàn)了 UIView 動畫的效果(不僅僅是動畫,還包括動畫結(jié)束后的處理),那么手勢結(jié)束后,動畫將直接跳轉(zhuǎn)到最終狀態(tài);而更多的一種狀況是,你并沒有正確地復現(xiàn) UIView 動畫的效果,手勢結(jié)束后動畫會停留在手勢中止時的狀態(tài),界面失去響應(yīng)。所以,如果你需要完美的交互轉(zhuǎn)場動畫,必須使用 UIView 動畫。
  2. 在動畫控制器的animateTransition:中提交動畫。問題和第1點類似,在viewWillDisappear:這樣的方法中提交的動畫也能被交互控制,但交互停止時,立即跳轉(zhuǎn)到最終狀態(tài)。

5 UICollectionViewController

布局轉(zhuǎn)場

布局轉(zhuǎn)場只針對 CollectionViewController 搭配 NavigationController 的組合,且是作用于布局,而非視圖。采用這種布局轉(zhuǎn)場時,NavigationController 將會用布局變化的動畫來替代 push 和 pop 的默認動畫。蘋果自家的照片應(yīng)用中的「照片」Tab 頁面使用了這個技術(shù):在「年度-精選-時刻」幾個時間模式間切換時,CollectionViewController 在 push 或 pop 時盡力維持在同一個元素的位置同時進行布局轉(zhuǎn)換。

布局轉(zhuǎn)場的實現(xiàn)比三大主流轉(zhuǎn)場要簡單得多,只需要滿足四個條件:NavigationController + CollectionViewController, 且要求后者都擁有相同數(shù)據(jù)源, 并且開啟useLayoutToLayoutNavigationTransitions屬性為真。

let cvc0 = UICollectionViewController(collectionViewLayout: layout0) //作為 root VC 的 cvc0 的該屬性必須為 false,該屬性默認為 false。 cvc0.useLayoutToLayoutNavigationTransitions = false let nav = UINavigationController(rootViewController: cvc0) //cvc0, cvc1, cvc2 必須具有相同的數(shù)據(jù),如果在某個時刻修改了其中的一個數(shù)據(jù)源,其他的數(shù)據(jù)源必須同步,不然會出錯。

let cvc1 = UICollectionViewController(collectionViewLayout: layout1) cvc1.useLayoutToLayoutNavigationTransitions = true 
nav.pushViewController(cvc1, animated: true) 
let cvc2 = UICollectionViewController(collectionViewLayout: layout2) cvc2.useLayoutToLayoutNavigationTransitions = true 
nav.pushViewController(cvc2, animated: true)
nav.popViewControllerAnimated(true) 

Push 進入控制器棧后,不能更改useLayoutToLayoutNavigationTransitions的值,否則應(yīng)用會崩潰。當 CollectionView 的數(shù)據(jù)源(section 和 cell 的數(shù)量)不完全一致時,push 和 pop 時依然會有布局轉(zhuǎn)場動畫,但是當 pop 回到 rootVC 時,應(yīng)用會崩潰。可否共享數(shù)據(jù)源保持同步來克服這個缺點?測試表明,這樣做可能會造成畫面上的殘缺,以及不穩(wěn)定,建議不要這么做。

此外,iOS 7 支持 UICollectionView 布局的交互轉(zhuǎn)換(Layout Interactive Transition),過程與控制器的交互轉(zhuǎn)場(ViewController Interactive Transition)類似,這個功能和布局轉(zhuǎn)場(CollectionViewController Layout Transition)容易混淆,前者是在自身布局轉(zhuǎn)換的基礎(chǔ)上實現(xiàn)了交互控制,后者是 CollectionViewController 與 NavigationController 結(jié)合后在轉(zhuǎn)場的同時進行布局轉(zhuǎn)換。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容