iOS UIViewControllerTransitioning 自定義轉(zhuǎn)場(chǎng)框架的使用
iOS UIViewControllerTransitioning 自定義轉(zhuǎn)場(chǎng)框架的使用 1
1 簡(jiǎn)介 1
2 轉(zhuǎn)場(chǎng)協(xié)議 2
2.1轉(zhuǎn)場(chǎng)代理(Transition Delegate): 2
2.2.動(dòng)畫(huà)控制器(Animation Controller): 2
2.3.交互控制器(Interaction Controller): 3
2.4.轉(zhuǎn)場(chǎng)環(huán)境(Transition Context): 3
2.5.轉(zhuǎn)場(chǎng)協(xié)調(diào)器(Transition Coordinator): 3
3 非交互式轉(zhuǎn)場(chǎng) 3
3.1 動(dòng)畫(huà)控制器協(xié)議 3
3.2 動(dòng)畫(huà)控制器實(shí)現(xiàn) 5
3.3 特殊的 Modal 轉(zhuǎn)場(chǎng) 6
4 交互式轉(zhuǎn)場(chǎng) 10
Transition Coordinator 13
交互轉(zhuǎn)場(chǎng)的限制 14
5 UICollectionViewController 布局轉(zhuǎn)場(chǎng) 15
1 簡(jiǎn)介
在 iOS 7 之前,我們只能使用系統(tǒng)提供的轉(zhuǎn)場(chǎng)效果桅锄,大部分時(shí)候夠用琉雳,但僅僅是夠用而已,總歸會(huì)有各種不如意的小地方友瘤,但我們卻無(wú)力改變翠肘;iOS 7 開(kāi)放了相關(guān) API 允許我們對(duì)轉(zhuǎn)場(chǎng)效果進(jìn)行全面定制,目前為止辫秧,官方支持以下幾種方式的自定義轉(zhuǎn)場(chǎng):
(1)、在 UINavigationController 中 push 和 pop;
(2)盟戏、在 UITabBarController 中切換 Tab;
(3)绪妹、Modal 轉(zhuǎn)場(chǎng):presentation 和 dismiss,俗稱視圖控制器的模態(tài)顯示和消失抓半,僅限于modalPresentationStyle屬性為 UIModalPresentationFullScreen 或 UIModalPresentationCustom 這兩種模式;
(4)喂急、UICollectionViewController 的布局轉(zhuǎn)場(chǎng);
2 轉(zhuǎn)場(chǎng)協(xié)議
iOS 7 以協(xié)議的方式開(kāi)放了自定義轉(zhuǎn)場(chǎng)的 API笛求,協(xié)議的好處是不再拘泥于具體的某個(gè)類(lèi)廊移,只要是遵守該協(xié)議的對(duì)象都能參與轉(zhuǎn)場(chǎng),非常靈活探入。轉(zhuǎn)場(chǎng)協(xié)議由5種協(xié)議組成狡孔,在實(shí)際中只需要我們提供其中的兩個(gè)或三個(gè)便能實(shí)現(xiàn)絕大部分的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)
2.1轉(zhuǎn)場(chǎng)代理(Transition Delegate):
自定義轉(zhuǎn)場(chǎng)的第一步便是提供轉(zhuǎn)場(chǎng)代理,告訴系統(tǒng)使用我們提供的代理而不是系統(tǒng)的默認(rèn)代理來(lái)執(zhí)行轉(zhuǎn)場(chǎng)蜂嗽。有如下三種轉(zhuǎn)場(chǎng)代理苗膝,對(duì)應(yīng)上面三種類(lèi)型的轉(zhuǎn)場(chǎng):
//1. UINavigationController 的 delegate 遵循的協(xié)議
UINavigationControllerDelegate。
//2. UITabBarController 的 delegate 遵循的協(xié)議植旧。
UITabBarControllerDelegate
// 3.UIViewController 的 transitioningDelegate 遵循的協(xié)議辱揭。
UIViewControllerTransitioningDelegate
這里除了<UIViewControllerTransitioningDelegate>是 iOS 7 新增的協(xié)議离唐,其他兩種在 iOS 2 里就存在了,在 iOS 7 時(shí)擴(kuò)充了這種協(xié)議來(lái)支持自定義轉(zhuǎn)場(chǎng)问窃。
轉(zhuǎn)場(chǎng)發(fā)生時(shí)亥鬓,UIKit 將要求轉(zhuǎn)場(chǎng)代理將提供轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的核心構(gòu)件:動(dòng)畫(huà)控制器和交互控制器(可選的)由我們實(shí)現(xiàn)。
2.2.動(dòng)畫(huà)控制器(Animation Controller):
最重要的部分域庇,負(fù)責(zé)添加視圖以及執(zhí)行動(dòng)畫(huà)嵌戈;遵守<UIViewControllerAnimatedTransitioning>協(xié)議;由我們實(shí)現(xiàn)。
2.3.交互控制器(Interaction Controller):
通過(guò)交互手段,通常是手勢(shì)來(lái)驅(qū)動(dòng)動(dòng)畫(huà)控制器實(shí)現(xiàn)的動(dòng)畫(huà)没咙,使得用戶能夠控制整個(gè)過(guò)程;遵守<UIViewControllerInteractiveTransitioning>協(xié)議于樟;系統(tǒng)已經(jīng)打包好現(xiàn)成的類(lèi)供我們使用。
2.4.轉(zhuǎn)場(chǎng)環(huán)境(Transition Context):
提供轉(zhuǎn)場(chǎng)中需要的數(shù)據(jù);遵守<UIViewControllerContextTransitioning>協(xié)議;由 UIKit 在轉(zhuǎn)場(chǎng)開(kāi)始前生成并提供給我們提交的動(dòng)畫(huà)控制器和交互控制器使用偿短。
2.5.轉(zhuǎn)場(chǎng)協(xié)調(diào)器(Transition Coordinator):
可在轉(zhuǎn)場(chǎng)動(dòng)畫(huà)發(fā)生的同時(shí)并行執(zhí)行其他的動(dòng)畫(huà),其作用與其說(shuō)協(xié)調(diào)不如說(shuō)輔助馋没,主要在 Modal 轉(zhuǎn)場(chǎng)和交互轉(zhuǎn)場(chǎng)取消時(shí)使用,其他時(shí)候很少用到降传;遵守<UIViewControllerTransitionCoordinator>協(xié)議篷朵;由 UIKit 在轉(zhuǎn)場(chǎng)時(shí)生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一個(gè)遵守該協(xié)議的對(duì)象婆排,且該方法只在該控制器處于轉(zhuǎn)場(chǎng)過(guò)程中才返回一個(gè)此類(lèi)對(duì)象声旺,不參與轉(zhuǎn)場(chǎng)時(shí)返回 nil。
3 非交互式轉(zhuǎn)場(chǎng)
這個(gè)階段要做兩件事段只,提供轉(zhuǎn)場(chǎng)代理并由代理提供動(dòng)畫(huà)控制器腮猖。在轉(zhuǎn)場(chǎng)代理協(xié)議里動(dòng)畫(huà)控制器和交互控制器都是可選實(shí)現(xiàn)的,沒(méi)有實(shí)現(xiàn)或者返回 nil 的話則使用默認(rèn)的轉(zhuǎn)場(chǎng)效果赞枕。
3.1 動(dòng)畫(huà)控制器協(xié)議
動(dòng)畫(huà)控制器負(fù)責(zé)添加視圖以及執(zhí)行動(dòng)畫(huà)澈缺,遵守UIViewControllerAnimatedTransitioning協(xié)議,該協(xié)議要求實(shí)現(xiàn)以下方法:
//執(zhí)行動(dòng)畫(huà)的地方炕婶,最核心的方法姐赡。
(Required)func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)
//返回動(dòng)畫(huà)時(shí)間,"return 0.5" 已足夠柠掂,非常簡(jiǎn)單
(Required)func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval
//如果實(shí)現(xiàn)了项滑,會(huì)在轉(zhuǎn)場(chǎng)動(dòng)畫(huà)結(jié)束后調(diào)用,可以執(zhí)行一些收尾工作涯贞。
(Optional)func animationEnded(_ transitionCompleted: Bool)
最重要的是第一個(gè)方法枪狂,該方法接受一個(gè)遵守<UIViewControllerContextTransitioning>協(xié)議的轉(zhuǎn)場(chǎng)環(huán)境對(duì)象危喉,上一節(jié)的 API 解釋里提到這個(gè)協(xié)議,它提供了轉(zhuǎn)場(chǎng)所需要的重要數(shù)據(jù):參與轉(zhuǎn)場(chǎng)的視圖控制器和轉(zhuǎn)場(chǎng)過(guò)程的狀態(tài)信息州疾。
UIKit 在轉(zhuǎn)場(chǎng)開(kāi)始前生成遵守轉(zhuǎn)場(chǎng)環(huán)境協(xié)議<UIViewControllerContextTransitioning>的對(duì)象 transitionContext姥饰,它有以下幾個(gè)方法來(lái)提供動(dòng)畫(huà)控制器需要的信息:
//返回容器視圖,轉(zhuǎn)場(chǎng)動(dòng)畫(huà)發(fā)生的地方孝治。 func containerView() -> UIView?
//獲取參與轉(zhuǎn)場(chǎng)的視圖控制器列粪,有 UITransitionContextFromViewControllerKey 和 UITransitionContextToViewControllerKey 兩個(gè) Key。
func viewControllerForKey(_ key: String) -> UIViewController? //iOS 8新增 API 用于方便獲取參與參與轉(zhuǎn)場(chǎng)的視圖谈飒,有 UITransitionContextFromViewKey 和 UITransitionContextToViewKey 兩個(gè) Key岂座。
func viewForKey(_ key: String) -> UIView? AVAILABLE_IOS(8_0)
通過(guò)viewForKey:獲取的視圖是viewControllerForKey:返回的控制器的根視圖,或者 nil杭措。viewForKey:方法返回 nil 只有一種情況: UIModalPresentationCustom 模式下的 Modal 轉(zhuǎn)場(chǎng) 费什,通過(guò)此方法獲取 presentingView 時(shí)得到的將是 nil,在后面的 Modal 轉(zhuǎn)場(chǎng)里會(huì)詳細(xì)解釋手素。
前面提到轉(zhuǎn)場(chǎng)的本質(zhì)是下一個(gè)場(chǎng)景的視圖替換當(dāng)前場(chǎng)景的視圖鸳址,從當(dāng)前場(chǎng)景過(guò)渡下一個(gè)場(chǎng)景。下面稱即將消失的場(chǎng)景的視圖為 fromView泉懦,對(duì)應(yīng)的視圖控制器為 fromVC稿黍,即將出現(xiàn)的視圖為 toView,對(duì)應(yīng)的視圖控制器稱之為 toVC崩哩。幾種轉(zhuǎn)場(chǎng)方式的轉(zhuǎn)場(chǎng)操作都是可逆的巡球,一種操作里的 fromView 和 toView 在逆向操作里的角色互換成對(duì)方,fromVC 和 toVC 也是如此邓嘹。 在動(dòng)畫(huà)控制器里酣栈,參與轉(zhuǎn)場(chǎng)的視圖只有 fromView 和 toView 之分,與轉(zhuǎn)場(chǎng)方式無(wú)關(guān)汹押。轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的最終效果只限制于你的想象力矿筝。 這也是動(dòng)畫(huà)控制器在封裝后可以被第三方使用的重要原因。
在 iOS 8 中可通過(guò)以下方法來(lái)獲取參與轉(zhuǎn)場(chǎng)的三個(gè)重要視圖棚贾,在 iOS 7 中則需要通過(guò)對(duì)應(yīng)的視圖控制器來(lái)獲取窖维,為避免 API 差異導(dǎo)致代碼過(guò)長(zhǎng),示例代碼中直接使用下面的視圖變量:
let containerView = transitionContext.containerView()
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)
3.2 動(dòng)畫(huà)控制器實(shí)現(xiàn)
轉(zhuǎn)場(chǎng) API 是協(xié)議的好處是不限制具體的類(lèi)鸟悴,只要對(duì)象實(shí)現(xiàn)該協(xié)議便能參與轉(zhuǎn)場(chǎng)過(guò)程陈辱,這也帶來(lái)另外一個(gè)好處:封裝便于復(fù)用,盡管三大轉(zhuǎn)場(chǎng)代理協(xié)議的方法不盡相同细诸,但它們返回的動(dòng)畫(huà)控制器遵守的是同一個(gè)協(xié)議沛贪,因此可以將動(dòng)畫(huà)控制器封裝作為第三方動(dòng)畫(huà)控制器在其他控制器的轉(zhuǎn)場(chǎng)過(guò)程中使用。
func animateTransition(transitionContext: UIViewControllerContextTransitioning) { ...
//1
containerView.addSubview(toView)
//計(jì)算位移 transform,NavigationVC 和 TabBarVC 在水平方向進(jìn)行動(dòng)畫(huà)利赋,Modal 轉(zhuǎn)場(chǎng)在豎直方向進(jìn)行動(dòng)畫(huà)水评。
var toViewTransform = ...
var fromViewTransform = ...
toView.transform = toViewTransform
//根據(jù)協(xié)議中的方法獲取動(dòng)畫(huà)的時(shí)間。
let duration = self.transitionDuration(transitionContext)
UIView.animateWithDuration(duration, animations: {
fromView.transform = fromViewTransform
toView.transform = CGAffineTransformIdentity
}, completion: { _ in
//考慮到轉(zhuǎn)場(chǎng)中途可能取消的情況媚送,轉(zhuǎn)場(chǎng)結(jié)束后中燥,恢復(fù)視圖狀態(tài)。
fromView.transform = CGAffineTransformIdentity
toView.transform = CGAffineTransformIdentity
//2
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
})
}
注意上面的代碼有2處標(biāo)記塘偎,是動(dòng)畫(huà)控制器必須完成的:
- 將 toView 添加到容器視圖中疗涉,使得 toView 在屏幕上顯示( Modal 轉(zhuǎn)場(chǎng)中此點(diǎn)稍有不同,下一節(jié)細(xì)述)吟秩;
- 正確地結(jié)束轉(zhuǎn)場(chǎng)過(guò)程咱扣。轉(zhuǎn)場(chǎng)的結(jié)果有兩種:完成或取消。非交互轉(zhuǎn)場(chǎng)的結(jié)果只有完成一種情況涵防,不過(guò)交互式轉(zhuǎn)場(chǎng)需要考慮取消的情況闹伪。如何結(jié)束取決于轉(zhuǎn)場(chǎng)的進(jìn)度,通過(guò)transitionWasCancelled()方法來(lái)獲取轉(zhuǎn)場(chǎng)的狀態(tài)壮池,使用completeTransition:來(lái)完成或取消轉(zhuǎn)場(chǎng)偏瓤。
3.3 特殊的 Modal 轉(zhuǎn)場(chǎng)
UINavigationController 和 UITabBarController 這兩個(gè)容器 VC 的根視圖在屏幕上是不可見(jiàn)的(或者說(shuō)是透明的),可見(jiàn)的只是內(nèi)嵌在這兩者中的子 VC 中的視圖椰憋,轉(zhuǎn)場(chǎng)是從子 VC 的視圖轉(zhuǎn)換到另外一個(gè)子 VC 的視圖厅克,其根視圖并未參與轉(zhuǎn)場(chǎng);而 Modal 轉(zhuǎn)場(chǎng)熏矿,以 presentation 為例已骇,是從 presentingView 轉(zhuǎn)換到 presentedView,根視圖 presentingView 也就是 fromView 參與了轉(zhuǎn)場(chǎng)票编。而且 NavigationController 和 TabBarController 轉(zhuǎn)場(chǎng)中的 containerView 也并非這兩者的根視圖。
[圖片上傳失敗...(image-d2e88f-1616030414999)]
Modal 轉(zhuǎn)場(chǎng)與兩種容器 VC 的轉(zhuǎn)場(chǎng)的另外一個(gè)不同是:Modal 轉(zhuǎn)場(chǎng)結(jié)束后 presentingView 可能依然可見(jiàn)卵渴,UIModalPresentationPageSheet 模式就是這樣慧域。這種不同導(dǎo)致了 Modal 轉(zhuǎn)場(chǎng)和容器 VC 的轉(zhuǎn)場(chǎng)對(duì) fromView 的處理差異:容器 VC 的轉(zhuǎn)場(chǎng)結(jié)束后 fromView 會(huì)被主動(dòng)移出視圖結(jié)構(gòu),這是可預(yù)見(jiàn)的結(jié)果浪读,我們也可以在轉(zhuǎn)場(chǎng)結(jié)束前手動(dòng)移除昔榴;而 Modal 轉(zhuǎn)場(chǎng)中,presentation 結(jié)束后 presentingView(fromView) 并未主動(dòng)被從視圖結(jié)構(gòu)中移除碘橘。準(zhǔn)確來(lái)說(shuō)互订,是 UIModalPresentationCustom 這種模式下的 Modal 轉(zhuǎn)場(chǎng)結(jié)束時(shí) fromView 并未從視圖結(jié)構(gòu)中移除;UIModalPresentationFullScreen 模式的 Modal 轉(zhuǎn)場(chǎng)結(jié)束后 fromView 依然主動(dòng)被從視圖結(jié)構(gòu)中移除了痘拆。這種差異導(dǎo)致在處理 dismissal 轉(zhuǎn)場(chǎng)的時(shí)候很容易出現(xiàn)問(wèn)題仰禽,沒(méi)有意識(shí)到這個(gè)不同點(diǎn)的話出錯(cuò)時(shí)就會(huì)毫無(wú)頭緒。下面來(lái)看看 dismissal 轉(zhuǎn)場(chǎng)時(shí)的場(chǎng)景。
ContainerView 在轉(zhuǎn)場(chǎng)期間作為 fromView 和 toView 的父視圖吐葵。三種轉(zhuǎn)場(chǎng)過(guò)程中的 containerView 是 UIView 的私有子類(lèi)规揪,不過(guò)我們并不需要關(guān)心 containerView 具體是什么。在 dismissal 轉(zhuǎn)場(chǎng)中:
- UIModalPresentationFullScreen 模式:presentation 后温峭,presentingView 被主動(dòng)移出視圖結(jié)構(gòu)猛铅,在 dismissal 中 presentingView 是 toView 的角色,其將會(huì)重新加入 containerView 中凤藏,實(shí)際上奸忽,我們不主動(dòng)將其加入,UIKit 也會(huì)這么做揖庄,前面的兩種容器控制器的轉(zhuǎn)場(chǎng)里不是這樣處理的栗菜,不過(guò)這個(gè)差異基本沒(méi)什么影響。
- UIModalPresentationCustom 模式:轉(zhuǎn)場(chǎng)時(shí) containerView 并不擔(dān)任 presentingView 的父視圖抠艾,后者由 UIKit 另行管理苛萎。在 presentation 后,fromView(presentingView) 未被移出視圖結(jié)構(gòu)检号,在 dismissal 中腌歉,注意不要像其他轉(zhuǎn)場(chǎng)中那樣將 toView(presentingView) 加入 containerView 中,否則本來(lái)可見(jiàn)的 presentingView 將會(huì)被移除出自身所處的視圖結(jié)構(gòu)消失不見(jiàn)齐苛。如果你在使用 Custom 模式時(shí)沒(méi)有注意到這點(diǎn)翘盖,就很容易掉進(jìn)這個(gè)陷阱而很難察覺(jué)問(wèn)題所在
對(duì)于 Custom 模式,我們可以參照其他轉(zhuǎn)場(chǎng)里的處理規(guī)則來(lái)打理:presentation 轉(zhuǎn)場(chǎng)結(jié)束后主動(dòng)將 fromView(presentingView) 移出它的視圖結(jié)構(gòu)凹蜂,并用一個(gè)變量來(lái)維護(hù) presentingView 的父視圖馍驯,以便在 dismissal 轉(zhuǎn)場(chǎng)中恢復(fù);在 dismissal 轉(zhuǎn)場(chǎng)中玛痊,presentingView 的角色由原來(lái)的 fromView 切換成了 toView汰瘫,我們?cè)賹⑵渲匦禄謴?fù)它原來(lái)的視圖結(jié)構(gòu)中。測(cè)試表明這樣做是可行的擂煞。但是這樣一來(lái)混弥,在實(shí)現(xiàn)上,需要在轉(zhuǎn)場(chǎng)代理中維護(hù)一個(gè)動(dòng)畫(huà)控制器并且這個(gè)動(dòng)畫(huà)控制器要維護(hù) presentingView 的父視圖对省,第三方的動(dòng)畫(huà)控制器必須為此改造蝗拿。顯然,這樣的代價(jià)是無(wú)法接受的蒿涎。
小結(jié) :經(jīng)過(guò)上面的嘗試哀托,建議是,不要干涉官方對(duì) Modal 轉(zhuǎn)場(chǎng)的處理劳秋,我們?nèi)ミm應(yīng)它仓手。在 Custom 模式下胖齐,由于 presentingView 不受 containerView 管理,在 dismissal 轉(zhuǎn)場(chǎng)中不要像其他的轉(zhuǎn)場(chǎng)那樣將 toView(presentingView) 加入 containerView俗或,否則 presentingView 將消失不見(jiàn)市怎,而應(yīng)用則也很可能假死;而在 presentation 轉(zhuǎn)場(chǎng)中辛慰,切記不要手動(dòng)將 fromView(presentingView) 移出其父視圖区匠。
iOS 8 為<UIViewControllerContextTransitioning>協(xié)議添加了viewForKey:方法以方便獲取 fromView 和 toView,但是在 Modal 轉(zhuǎn)場(chǎng)里要注意帅腌,從上面可以知道驰弄,Custom 模式下,presentingView 并不受 containerView 管理速客,這時(shí)通過(guò)viewForKey:方法來(lái)獲取 presentingView 得到的是 nil戚篙,必須通過(guò)viewControllerForKey:得到 presentingVC 后來(lái)獲取。因此在 Modal 轉(zhuǎn)場(chǎng)中溺职,較穩(wěn)妥的方法是從 fromVC 和 toVC 中獲取 fromView 和 toView岔擂。
順帶一提,前面提到的UIView的類(lèi)方法transitionFromView:toView:duration:options:completion:能在 Custom 模式下工作浪耘,卻與 FullScreen 模式有點(diǎn)不兼容乱灵。
Model 轉(zhuǎn)場(chǎng)實(shí)現(xiàn):
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
...
//不像容器 VC 轉(zhuǎn)場(chǎng)里需要額外的變量來(lái)標(biāo)記操作類(lèi)型,UIViewController 自身就有方法跟蹤 Modal 狀態(tài)七冲。
//處理 Presentation 轉(zhuǎn)場(chǎng):
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)
//實(shí)現(xiàn)出現(xiàn)時(shí)的尺寸變化的動(dòng)畫(huà):
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)場(chǎng)蝉稳,按照上一小節(jié)的結(jié)論,.Custom 模式下不要將 toView 添加到 containerView掘鄙,省去了上面標(biāo)記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)場(chǎng)
在非交互轉(zhuǎn)場(chǎng)的基礎(chǔ)上將之交互化需要兩個(gè)條件:
- 由轉(zhuǎn)場(chǎng)代理提供交互控制器,這是一個(gè)遵守<UIViewControllerInteractiveTransitioning>協(xié)議的對(duì)象操漠,不過(guò)系統(tǒng)已經(jīng)打包好了現(xiàn)成的類(lèi)UIPercentDrivenInteractiveTransition供我們使用毕莱。我們不需要做任何配置,僅僅在轉(zhuǎn)場(chǎng)代理的相應(yīng)方法中提供一個(gè)該類(lèi)實(shí)例便能工作颅夺。另外交互控制器必須有動(dòng)畫(huà)控制器才能工作。
- 交互控制器還需要交互手段的配合蛹稍,最常見(jiàn)的是使用手勢(shì)吧黄,或是其他事件,來(lái)驅(qū)動(dòng)整個(gè)轉(zhuǎn)場(chǎng)進(jìn)程唆姐。
滿足以上兩個(gè)條件很簡(jiǎn)單拗慨,但是很容易犯錯(cuò)誤。
正確地提供交互控制器 :
如果在轉(zhuǎn)場(chǎng)代理中提供了交互控制器,而轉(zhuǎn)場(chǎng)發(fā)生時(shí)并沒(méi)有方法來(lái)驅(qū)動(dòng)轉(zhuǎn)場(chǎng)進(jìn)程(比如手勢(shì))赵抢,轉(zhuǎn)場(chǎng)過(guò)程將一直處于開(kāi)始階段無(wú)法結(jié)束剧蹂,應(yīng)用界面也會(huì)失去響應(yīng):在 NavigationController 中點(diǎn)擊 NavigationBar 也能實(shí)現(xiàn) pop 返回操作,但此時(shí)沒(méi)有了交互手段的支持烦却,轉(zhuǎn)場(chǎng)過(guò)程卡殼宠叼;在 TabBarController 的代理里提供交互控制器存在同樣的問(wèn)題,點(diǎn)擊 TabBar 切換頁(yè)面時(shí)也沒(méi)有實(shí)現(xiàn)交互控制其爵。因此僅在確實(shí)處于交互狀態(tài)時(shí)才提供交互控制器冒冬,可以使用一個(gè)變量來(lái)標(biāo)記交互狀態(tài),該變量由交互手勢(shì)來(lái)更新?tīng)顟B(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 的實(shí)現(xiàn)類(lèi)似简烤,Modal 轉(zhuǎn)場(chǎng)代理分別為 presentation 和 dismissal 提供了各自的交互控制器,也需要注意上面的問(wèn)題摇幻。
問(wèn)題的根源是交互控制的工作機(jī)制導(dǎo)致的横侦,交互過(guò)程實(shí)際上是由轉(zhuǎn)場(chǎng)環(huán)境對(duì)象<UIViewControllerContextTransitioning>來(lái)管理的,它提供了如下幾個(gè)方法來(lái)控制轉(zhuǎn)場(chǎng)的進(jìn)度:
func updateInteractiveTransition(_ percentComplete: CGFloat)//更新轉(zhuǎn)場(chǎng)進(jìn)度绰姻,進(jìn)度數(shù)值范圍為0.0~1.0枉侧。 func cancelInteractiveTransition()//取消轉(zhuǎn)場(chǎng),轉(zhuǎn)場(chǎng)動(dòng)畫(huà)從當(dāng)前狀態(tài)返回至轉(zhuǎn)場(chǎng)發(fā)生前的狀態(tài)龙宏。 func finishInteractiveTransition()//完成轉(zhuǎn)場(chǎng)棵逊,轉(zhuǎn)場(chǎng)動(dòng)畫(huà)從當(dāng)前狀態(tài)繼續(xù)直至結(jié)束。
交互控制協(xié)議<UIViewControllerInteractiveTransitioning>只有一個(gè)必須實(shí)現(xiàn)的方法:
func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning)
在轉(zhuǎn)場(chǎng)代理里提供了交互控制器后银酗,轉(zhuǎn)場(chǎng)開(kāi)始時(shí)辆影,該方法自動(dòng)被 UIKit 調(diào)用對(duì)轉(zhuǎn)場(chǎng)環(huán)境進(jìn)行配置。
系統(tǒng)打包好的UIPercentDrivenInteractiveTransition中的控制轉(zhuǎn)場(chǎng)進(jìn)度的方法與轉(zhuǎn)場(chǎng)環(huán)境對(duì)象提供的三個(gè)方法同名黍特,實(shí)際上只是前者調(diào)用了后者的方法而已蛙讥。系統(tǒng)以一種解耦的方式使得動(dòng)畫(huà)控制器,交互控制器灭衷,轉(zhuǎn)場(chǎng)環(huán)境對(duì)象互相協(xié)作次慢,我們只需要使用UIPercentDrivenInteractiveTransition的三個(gè)同名方法來(lái)控制進(jìn)度就夠了。如果你要實(shí)現(xiàn)自己的交互控制器翔曲,而不是UIPercentDrivenInteractiveTransition的子類(lèi)迫像,就需要調(diào)用轉(zhuǎn)場(chǎng)環(huán)境的三個(gè)方法來(lái)控制進(jìn)度,壓軸環(huán)節(jié)我們將示范如何做瞳遍。
交互控制器控制轉(zhuǎn)場(chǎng)的過(guò)程就像將動(dòng)畫(huà)控制器實(shí)現(xiàn)的動(dòng)畫(huà)制作成一部視頻闻妓,我們使用手勢(shì)或是其他方法來(lái)控制轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的播放,可以前進(jìn)掠械,后退由缆,繼續(xù)或者停止注祖。finishInteractiveTransition()方法被調(diào)用后,轉(zhuǎn)場(chǎng)動(dòng)畫(huà)從當(dāng)前的狀態(tài)將繼續(xù)進(jìn)行直到動(dòng)畫(huà)結(jié)束均唉,轉(zhuǎn)場(chǎng)完成是晨;cancelInteractiveTransition()被調(diào)用后,轉(zhuǎn)場(chǎng)動(dòng)畫(huà)從當(dāng)前的狀態(tài)回?fù)艿匠跏紶顟B(tài)舔箭,轉(zhuǎn)場(chǎng)取消罩缴。
在 NavigationController 中點(diǎn)擊 NavigationBar 的 backBarButtomItem 執(zhí)行 pop 操作時(shí),由于我們無(wú)法介入 backBarButtomItem 的內(nèi)部流程限嫌,就失去控制進(jìn)度的手段靴庆,于是轉(zhuǎn)場(chǎng)過(guò)程只有一個(gè)開(kāi)始,永遠(yuǎn)不會(huì)結(jié)束怒医。其實(shí)我們只需要有能夠執(zhí)行上述幾個(gè)方法的手段就可以對(duì)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)進(jìn)行控制炉抒,用戶與屏幕的交互手段里,手勢(shì)是實(shí)現(xiàn)這個(gè)控制過(guò)程的天然手段稚叹,我猜這是其被稱為交互控制器的原因焰薄。
交互手段的配合 :
下面使用演示如何利用屏幕邊緣滑動(dòng)手勢(shì)UIScreenEdgePanGestureRecognizer在 NavigationController 中控制 Slide 動(dòng)畫(huà)控制器提供的動(dòng)畫(huà)來(lái)實(shí)現(xiàn)右滑返回的效果,該手勢(shì)綁定的動(dòng)作方法如下:
func handleEdgePanGesture(gesture: UIScreenEdgePanGestureRecognizer){ //根據(jù)移動(dòng)距離計(jì)算交互過(guò)程的進(jìn)度扒袖。 let percent = ... switch gesture.state{ case .Began: //轉(zhuǎn)場(chǎng)開(kāi)始前獲取代理塞茅,一旦轉(zhuǎn)場(chǎng)開(kāi)始,VC 將脫離控制器棧季率,此后 self.navigationController 返回的是 nil野瘦。 self.navigationDelegate = self.navigationController?.delegate as? SDENavigationDelegate //更新交互狀態(tài) self.navigationDelegate?.interactive = true //1.交互控制器沒(méi)有 start 之類(lèi)的方法飒泻,當(dāng)下面這行代碼執(zhí)行后,轉(zhuǎn)場(chǎng)開(kāi)始惰许; //如果轉(zhuǎn)場(chǎng)代理提供了交互控制器,它將從這時(shí)候開(kāi)始接管轉(zhuǎn)場(chǎng)過(guò)程汹买。 self.navigationController?.popViewControllerAnimated(true) case .Changed: //2.更新進(jìn)度: self.navigationDelegate?.interactionController.updateInteractiveTransition(percent) case .Cancelled, .Ended: //3.結(jié)束轉(zhuǎn)場(chǎng): if percent > 0.5{ //完成轉(zhuǎn)場(chǎng)聊倔。 self.navigationDelegate?.interactionController.finishInteractiveTransition() }else{ //或者,取消轉(zhuǎn)場(chǎng)结序。 self.navigationDelegate?.interactionController.cancelInteractiveTransition() } //無(wú)論轉(zhuǎn)場(chǎng)的結(jié)果如何,恢復(fù)為非交互狀態(tài)纵潦。 self.navigationDelegate?.interactive = false default: self.navigationDelegate?.interactive = false } }
交互轉(zhuǎn)場(chǎng)的流程就是三處數(shù)字標(biāo)記的代碼徐鹤。不管是什么交互方式,使用什么轉(zhuǎn)場(chǎng)方式邀层,都是在使用這三個(gè)方法控制轉(zhuǎn)場(chǎng)的進(jìn)度返敬。 對(duì)于交互式轉(zhuǎn)場(chǎng),交互手段只是表現(xiàn)形式寥院,本質(zhì)是驅(qū)動(dòng)轉(zhuǎn)場(chǎng)進(jìn)程劲赠。 很希望能夠看到更新穎的交互手法,比如通過(guò)點(diǎn)擊頁(yè)面不同區(qū)域來(lái)控制一套復(fù)雜的流程動(dòng)畫(huà)秸谢。TabBarController 的 Demo 中也實(shí)現(xiàn)了滑動(dòng)切換 Tab 頁(yè)面凛澎,代碼是類(lèi)似的,就不占篇幅了估蹄;示范的 Modal 轉(zhuǎn)場(chǎng)我沒(méi)有為之實(shí)現(xiàn)交互控制塑煎,原因也提到過(guò)了,沒(méi)有比較合乎操作直覺(jué)的交互手段臭蚁,不過(guò)真要為其添加交互控制最铁,代碼和上面是類(lèi)似的。
轉(zhuǎn)場(chǎng)交互化后結(jié)果有兩種:完成和取消垮兑。取消后動(dòng)畫(huà)將會(huì)原路返回到初始狀態(tài)冷尉,但已經(jīng)變化了的數(shù)據(jù)怎么恢復(fù)?
一種情況是系枪,控制器的系統(tǒng)屬性雀哨,比如,在 TabBarController 里使用上面的方法實(shí)現(xiàn)滑動(dòng)切換 Tab 頁(yè)面私爷,中途取消的話雾棺,已經(jīng)變化的selectedIndex屬性該怎么恢復(fù)為原值;上面的代碼里当犯,取消轉(zhuǎn)場(chǎng)的代碼執(zhí)行后垢村,self.navigationController返回的依然還是是 nil嘉栓,怎么讓控制器回到 NavigationController 的控制器棧頂侵佃。對(duì)于這種情況馋辈,UIKit 自動(dòng)替我們恢復(fù)了迈螟,不需要我們操心(可能你都沒(méi)有意識(shí)到這回事)答毫;
另外一種就是消返,轉(zhuǎn)場(chǎng)發(fā)生的過(guò)程中撵颊,你可能想實(shí)現(xiàn)某些效果倡勇,一般是在下面的事件中執(zhí)行译隘,轉(zhuǎn)場(chǎng)中途取消的話可能需要取消這些效果固耘。
func viewWillAppear(_ animated: Bool) func viewDidAppear(_ animated: Bool) func viewWillDisappear(_ animated: Bool) func viewDidDisappear(_ animated: Bool)
交互轉(zhuǎn)場(chǎng)介入后,視圖在這些狀態(tài)間的轉(zhuǎn)換變得復(fù)雜损敷,WWDC 上蘋(píng)果的工程師還表示轉(zhuǎn)場(chǎng)過(guò)程中 view 的Will系方法和Did系方法的執(zhí)行順序并不能得到保證拗馒,雖然幾率很小诱桂,但如果你依賴于這些方法執(zhí)行的順序的話就可能需要注意這點(diǎn)挥等。而且,Did系方法調(diào)用時(shí)并不意味著轉(zhuǎn)場(chǎng)過(guò)程真的結(jié)束了。另外掷漱,fromView 和 toView 之間的這幾種方法的相對(duì)順序更加混亂,具體的案例可以參考這里:The Inconsistent Order of View Transition Events。
如何在轉(zhuǎn)場(chǎng)過(guò)程中的任意階段中斷時(shí)取消不需要的效果缰冤?這時(shí)候該轉(zhuǎn)場(chǎng)協(xié)調(diào)器(Transition Coordinator)再次出場(chǎng)了怀薛。
Transition Coordinator
轉(zhuǎn)場(chǎng)協(xié)調(diào)器(Transition Coordinator)的出場(chǎng)機(jī)會(huì)不多枝恋,但卻是關(guān)鍵先生焚碌。Modal
轉(zhuǎn)場(chǎng)中十电,UIPresentationController類(lèi)只能通過(guò)轉(zhuǎn)場(chǎng)協(xié)調(diào)器來(lái)與動(dòng)畫(huà)控制器同步,并行執(zhí)行其他動(dòng)畫(huà)畏线;這里它可以在交互式轉(zhuǎn)場(chǎng)結(jié)束時(shí)執(zhí)行一個(gè)閉包:
func notifyWhenInteractionEndsUsingBlock(_ handler: (UIViewControllerTransitionCoordinatorContext) -> Void)
當(dāng)轉(zhuǎn)場(chǎng)由交互狀態(tài)轉(zhuǎn)變?yōu)榉墙换顟B(tài)(在手勢(shì)交互過(guò)程中則為手勢(shì)結(jié)束時(shí))寝殴,無(wú)論轉(zhuǎn)場(chǎng)的結(jié)果是完成還是被取消杯矩,該方法都會(huì)被調(diào)用史隆;得益于閉包泌射,轉(zhuǎn)場(chǎng)協(xié)調(diào)器可以在轉(zhuǎn)場(chǎng)過(guò)程中的任意階段搜集動(dòng)作并在交互中止后執(zhí)行熔酷。閉包中的參數(shù)是一個(gè)遵守<UIViewControllerTransitionCoordinatorContext>協(xié)議的對(duì)象拒秘,該對(duì)象由 UIKit 提供躺酒,和前面的轉(zhuǎn)場(chǎng)環(huán)境對(duì)象<UIViewControllerContextTransitioning>作用類(lèi)似,它提供了交互轉(zhuǎn)場(chǎng)的狀態(tài)信息揽碘。
override func viewWillAppear(animated: Bool) { super.viewWillDisappear(animated) self.doSomeSideEffectsAssumingViewDidAppearIsGoingToBeCalled() //只在處于交互轉(zhuǎn)場(chǎng)過(guò)程中才可能取消效果。 if let coordinator = self.transitionCoordinator() where coordinator.initiallyInteractive() == true{ coordinator.notifyWhenInteractionEndsUsingBlock({ interactionContext in if interactionContext.isCancelled(){ self.undoSideEffects() } }) } }
不過(guò)交互狀態(tài)結(jié)束時(shí)并非轉(zhuǎn)場(chǎng)過(guò)程的終點(diǎn)(此后動(dòng)畫(huà)控制器提供的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)根據(jù)交互結(jié)束時(shí)的狀態(tài)繼續(xù)或是返回到初始狀態(tài))掖桦,而是由動(dòng)畫(huà)控制器來(lái)結(jié)束這一切:
optional func animationEnded(_ transitionCompleted: Bool)
如果實(shí)現(xiàn)了該方法滞详,將在轉(zhuǎn)場(chǎng)動(dòng)畫(huà)結(jié)束后調(diào)用料饥。
UIViewController 可以通過(guò)transitionCoordinator()獲取轉(zhuǎn)場(chǎng)協(xié)調(diào)器岸啡,該方法的文檔中說(shuō)只有在 Modal 轉(zhuǎn)場(chǎng)過(guò)程中巡蘸,該方法才返回一個(gè)與當(dāng)前轉(zhuǎn)場(chǎng)相關(guān)的有效對(duì)象悦荒。實(shí)際上搬味,NavigationController 的轉(zhuǎn)場(chǎng)中 fromVC 和 toVC 也能返回一個(gè)有效對(duì)象萍聊,TabBarController 有點(diǎn)特殊寿桨,fromVC 和 toVC 在轉(zhuǎn)場(chǎng)中返回的是 nil亭螟,但是作為容器的 TabBarController 可以使用該方法返回一個(gè)有效對(duì)象媒佣。
轉(zhuǎn)場(chǎng)協(xié)調(diào)器除了上面的兩種關(guān)鍵作用外,也在 iOS 8 中的適應(yīng)性布局中擔(dān)任重要角色衰琐,可以查看<UIContentContainer>協(xié)議中的方法羡宙,其中響應(yīng)尺寸和屏幕旋轉(zhuǎn)事件的方法都包含一個(gè)轉(zhuǎn)場(chǎng)協(xié)調(diào)器對(duì)象狗热,視圖的這種變化也被系統(tǒng)視為廣義上的 transition匿刮,參數(shù)中的轉(zhuǎn)場(chǎng)協(xié)調(diào)器也由 UIKit 提供。這個(gè)話題有點(diǎn)超出本文的范圍光羞,就不深入了怀大,有需要的話可以查看文檔和相關(guān) session潜慎。
交互轉(zhuǎn)場(chǎng)的限制
如果希望轉(zhuǎn)場(chǎng)中的動(dòng)畫(huà)能完美地被交互控制局服,必須滿足2個(gè)隱性條件:
- 使用 UIView 動(dòng)畫(huà)的 API淫奔。你當(dāng)然也可以使用 Core Animation 來(lái)實(shí)現(xiàn)動(dòng)畫(huà)唆迁,甚至唐责,這種動(dòng)畫(huà)可以被交互控制鼠哥,但是當(dāng)交互中止時(shí)朴恳,會(huì)出現(xiàn)一些意外情況:如果你正確地用 Core Animation 的方式復(fù)現(xiàn)了 UIView 動(dòng)畫(huà)的效果(不僅僅是動(dòng)畫(huà)于颖,還包括動(dòng)畫(huà)結(jié)束后的處理)森渐,那么手勢(shì)結(jié)束后同衣,動(dòng)畫(huà)將直接跳轉(zhuǎn)到最終狀態(tài)乳怎;而更多的一種狀況是,你并沒(méi)有正確地復(fù)現(xiàn) UIView 動(dòng)畫(huà)的效果询枚,手勢(shì)結(jié)束后動(dòng)畫(huà)會(huì)停留在手勢(shì)中止時(shí)的狀態(tài)金蜀,界面失去響應(yīng)尝胆。所以含衔,如果你需要完美的交互轉(zhuǎn)場(chǎng)動(dòng)畫(huà)谓娃,必須使用 UIView 動(dòng)畫(huà)。
- 在動(dòng)畫(huà)控制器的animateTransition:中提交動(dòng)畫(huà)痰憎。問(wèn)題和第1點(diǎn)類(lèi)似,在viewWillDisappear:這樣的方法中提交的動(dòng)畫(huà)也能被交互控制,但交互停止時(shí)据德,立即跳轉(zhuǎn)到最終狀態(tài)棘利。
5 UICollectionViewController
布局轉(zhuǎn)場(chǎng)
布局轉(zhuǎn)場(chǎng)只針對(duì) CollectionViewController 搭配 NavigationController 的組合善玫,且是作用于布局,而非視圖或渤。采用這種布局轉(zhuǎn)場(chǎng)時(shí)掌敬,NavigationController 將會(huì)用布局變化的動(dòng)畫(huà)來(lái)替代 push 和 pop 的默認(rèn)動(dòng)畫(huà)奔害。蘋(píng)果自家的照片應(yīng)用中的「照片」Tab 頁(yè)面使用了這個(gè)技術(shù):在「年度-精選-時(shí)刻」幾個(gè)時(shí)間模式間切換時(shí)芯杀,CollectionViewController 在 push 或 pop 時(shí)盡力維持在同一個(gè)元素的位置同時(shí)進(jìn)行布局轉(zhuǎn)換瘪匿。
布局轉(zhuǎn)場(chǎng)的實(shí)現(xiàn)比三大主流轉(zhuǎn)場(chǎng)要簡(jiǎn)單得多寻馏,只需要滿足四個(gè)條件:NavigationController + CollectionViewController, 且要求后者都擁有相同數(shù)據(jù)源棋弥, 并且開(kāi)啟useLayoutToLayoutNavigationTransitions屬性為真。
let cvc0 = UICollectionViewController(collectionViewLayout: layout0) //作為 root VC 的 cvc0 的該屬性必須為 false诚欠,該屬性默認(rèn)為 false顽染。 cvc0.useLayoutToLayoutNavigationTransitions = false let nav = UINavigationController(rootViewController: cvc0) //cvc0, cvc1, cvc2 必須具有相同的數(shù)據(jù),如果在某個(gè)時(shí)刻修改了其中的一個(gè)數(shù)據(jù)源轰绵,其他的數(shù)據(jù)源必須同步粉寞,不然會(huì)出錯(cuò)。
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 進(jìn)入控制器棧后左腔,不能更改useLayoutToLayoutNavigationTransitions的值鞭莽,否則應(yīng)用會(huì)崩潰。當(dāng) CollectionView 的數(shù)據(jù)源(section 和 cell 的數(shù)量)不完全一致時(shí)惧辈,push 和 pop 時(shí)依然會(huì)有布局轉(zhuǎn)場(chǎng)動(dòng)畫(huà),但是當(dāng) pop 回到 rootVC 時(shí)审洞,應(yīng)用會(huì)崩潰南吮⊥垦可否共享數(shù)據(jù)源保持同步來(lái)克服這個(gè)缺點(diǎn)幢哨?測(cè)試表明,這樣做可能會(huì)造成畫(huà)面上的殘缺屎慢,以及不穩(wěn)定,建議不要這么做欣喧。
此外,iOS 7 支持 UICollectionView 布局的交互轉(zhuǎn)換(Layout Interactive Transition),過(guò)程與控制器的交互轉(zhuǎn)場(chǎng)(ViewController Interactive Transition)類(lèi)似,這個(gè)功能和布局轉(zhuǎn)場(chǎng)(CollectionViewController Layout Transition)容易混淆豪筝,前者是在自身布局轉(zhuǎn)換的基礎(chǔ)上實(shí)現(xiàn)了交互控制,后者是 CollectionViewController 與 NavigationController 結(jié)合后在轉(zhuǎn)場(chǎng)的同時(shí)進(jìn)行布局轉(zhuǎn)換。