前言的前言
唐巧前輩在微信公眾號(hào)「iOSDevTips」以及其博客上推送了我的文章后,我的 Github 各項(xiàng)指標(biāo)有了大幅度的增長(zhǎng),多謝唐巧前輩的推薦。有些人問(wèn)我相關(guān)的問(wèn)題,好吧,目前為止就幾個(gè),由于沒(méi)有評(píng)論系統(tǒng),實(shí)在不方便交流,但我也沒(méi)把博客好好整理,一直都在簡(jiǎn)書(shū)上寫(xiě)博客,大家有問(wèn)題請(qǐng)移步我的簡(jiǎn)書(shū)本文章的頁(yè)面。關(guān)于交流,我想說(shuō)這么幾點(diǎn):
1.問(wèn)問(wèn)題就好,不要加上大神大牛之類的稱呼,與本文有關(guān)的問(wèn)題我盡量回答;不負(fù)責(zé)解析轉(zhuǎn)場(chǎng)動(dòng)畫(huà),看心情回答。
2.去我的簡(jiǎn)書(shū)下留言是最有效的交流方式,要加我好友就免了。
3.本文有一定的閱讀門檻,并非適合新手的手把手入門教程,更適合照著教程寫(xiě)過(guò)幾次轉(zhuǎn)場(chǎng)動(dòng)畫(huà)過(guò)了幾個(gè)月又忘了整個(gè)流程的人回顧學(xué)習(xí)精進(jìn)。本文的結(jié)構(gòu)以及相關(guān)知識(shí)點(diǎn)能讓你回憶起當(dāng)初親手寫(xiě)出轉(zhuǎn)場(chǎng)動(dòng)畫(huà)時(shí)的那股激動(dòng),除此之外,本文能滿足你希望徹底搞懂轉(zhuǎn)場(chǎng)的求知欲,我相信后者更重要,那種把分支技能樹(shù)升滿的感覺(jué)......
4.怎么提問(wèn)?新手如果覺(jué)得本文的范例啃不下去,去看源碼,很簡(jiǎn)單。如果是關(guān)于轉(zhuǎn)場(chǎng)動(dòng)畫(huà)中關(guān)鍵流程的地方,我相信本文已經(jīng)做出了很好的解釋,多讀幾遍;如果 Demo 里出了 Bug,請(qǐng)自己先確認(rèn)好,然后在Demo issue這里提交 issue 并給出你的詳細(xì)測(cè)試環(huán)境;如果對(duì)本文中探討機(jī)制以及缺陷的地方有疑問(wèn),歡迎留言交流。
屏幕左邊緣右滑返回,TabBar 滑動(dòng)切換,你是否喜歡并十分依賴這兩個(gè)操作,甚至覺(jué)得 App 不支持這類操作的話簡(jiǎn)直反人類?這兩個(gè)操作在大屏?xí)r代極大提升了操作效率,其背后的技術(shù)便是今天的主題:視圖控制器轉(zhuǎn)換(View Controller Transition)。
視圖控制器中的視圖顯示在屏幕上有兩種方式:最主要的方式是內(nèi)嵌在容器控制器中,比如 UINavigationController,UITabBarController, UISplitController;由另外一個(gè)視圖控制器顯示它,這種方式通常被稱為模態(tài)(Modal)顯示。View Controller Transition 是什么?在 NavigationController 里 push 或 pop 一個(gè) View Controller,在 TabBarController 中切換到其他 View Controller,以 Modal 方式顯示另外一個(gè) View Controller,這些都是 View Controller Transition。在 storyboard 里,每個(gè) View Controller 是一個(gè) Scene,View Controller Transition 便是從一個(gè) Scene 轉(zhuǎn)換到另外一個(gè) Scene;為方便,以下對(duì) View Controller Transition 的中文稱呼采用 Objccn.io 中的翻譯「轉(zhuǎn)場(chǎng)」。
在 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)配合動(dòng)畫(huà)以及對(duì)交互手段的支持帶來(lái)了無(wú)限可能,像開(kāi)頭提到的兩種轉(zhuǎn)場(chǎng)搭配簡(jiǎn)單的動(dòng)畫(huà)帶來(lái)了便利的交互操作,有些轉(zhuǎn)場(chǎng)配合華麗的動(dòng)畫(huà)則能讓轉(zhuǎn)場(chǎng)變得賞心悅目。
我知道你更想知道如何實(shí)現(xiàn)好看的轉(zhuǎn)場(chǎng)動(dòng)畫(huà),不過(guò)本文并非華麗的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)教程,相反,文中的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)效果都十分簡(jiǎn)單,但我會(huì)教你徹底掌握轉(zhuǎn)場(chǎng)動(dòng)畫(huà)中轉(zhuǎn)場(chǎng)的那部分,包括轉(zhuǎn)場(chǎng)背后的機(jī)制,缺陷以及實(shí)現(xiàn)過(guò)程中的技巧與陷阱。閱讀本文需要讀者至少要對(duì) ViewController 和 View 的結(jié)構(gòu)以及協(xié)議有基本的了解,最好自己親手實(shí)現(xiàn)過(guò)一兩種轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。如果你對(duì)此感覺(jué)沒(méi)有信心,推薦觀看官方文檔:View Controller Programming Guide for iOS,學(xué)習(xí)此文檔將會(huì)讓你更容易理解本文的內(nèi)容。對(duì)你想學(xué)習(xí)的小節(jié),我希望你自己親手寫(xiě)下這些代碼,一步步地看著效果是如何實(shí)現(xiàn)的,至少對(duì)我而言,看各種相關(guān)資料時(shí)只有字面意義上的理解,正是一步步的試驗(yàn)才能讓我理解每一個(gè)步驟。本文涉及的內(nèi)容較多,為了避免篇幅過(guò)長(zhǎng),我只給出關(guān)鍵代碼而不是從新建工程開(kāi)始教你每一個(gè)步驟。本文基于 Xcode 7 以及 Swift 2,Demo 合集地址:iOS-ViewController-Transition-Demo。
文章越來(lái)越長(zhǎng)了,分成三個(gè)部分:
第一部分:
iOS 8 的改進(jìn):UIPresentationController
第二部分:PartII Link(以下目錄無(wú)法跳轉(zhuǎn),請(qǐng)點(diǎn)擊該鏈接查看內(nèi)容)
第三部分:PartIII Link(以下目錄無(wú)法跳轉(zhuǎn),請(qǐng)點(diǎn)擊該鏈接查看內(nèi)容)
插曲:UICollectionViewController 布局轉(zhuǎn)場(chǎng)
動(dòng)畫(huà)控制和 CAMediaTiming 協(xié)議
尾聲:轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的設(shè)計(jì)
目前為止,官方支持以下幾種方式的自定義轉(zhuǎn)場(chǎng):
在 UINavigationController 中 push 和 pop;
在 UITabBarController 中切換 Tab;
Modal 轉(zhuǎn)場(chǎng):presentation 和 dismissal,俗稱視圖控制器的模態(tài)顯示和消失,僅限于modalPresentationStyle屬性為 UIModalPresentationFullScreen 或 UIModalPresentationCustom 這兩種模式;
UICollectionViewController 的布局轉(zhuǎn)場(chǎng):僅限于 UICollectionViewController 與 UINavigationController 結(jié)合的轉(zhuǎn)場(chǎng)方式,與上面三種都有點(diǎn)不同,不過(guò)實(shí)現(xiàn)很簡(jiǎn)單,可跳轉(zhuǎn)至該鏈接查看。
官方的支持包含了 iOS 中的大部分轉(zhuǎn)場(chǎng)方式,還有一種自定義容器中的轉(zhuǎn)場(chǎng)并沒(méi)有得到系統(tǒng)的直接支持,不過(guò)借助協(xié)議這種靈活的方式,我們依然能夠?qū)崿F(xiàn)對(duì)自定義容器控制器轉(zhuǎn)場(chǎng)的定制,在壓軸環(huán)節(jié)我們將實(shí)現(xiàn)這一點(diǎn)。
以上前三種轉(zhuǎn)場(chǎng)都需要轉(zhuǎn)場(chǎng)代理和動(dòng)畫(huà)控制器(見(jiàn)下節(jié))的幫助才能實(shí)現(xiàn)自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà),而觸發(fā)的方式分為三種:代碼里調(diào)用相關(guān)動(dòng)作的方法,Segue 以及,對(duì)于上面兩種容器 VC,在 UINavigationBar 和 UITabBar 上的相關(guān) Item 的點(diǎn)擊操作。
相關(guān)動(dòng)作方法
UINavigationController 中所有修改其viewControllers棧中 VC 的方法都可以自定義轉(zhuǎn)場(chǎng)動(dòng)畫(huà):
//我們使用的最廣泛的 push 和 pop 方法
func pushViewController(_ viewController: UIViewController, animated animated: Bool)
func popViewControllerAnimated(_ animated: Bool) -> UIViewController?
//不怎么常用的 pop 方法
func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]?
func popToRootViewControllerAnimated(_ animated: Bool) -> [UIViewController]?
//這個(gè)方法有有點(diǎn)特別,是對(duì) VC 棧的整體更新,開(kāi)啟動(dòng)畫(huà)后的執(zhí)行比較復(fù)雜,具體參考文檔說(shuō)明。不建議在這種情況下開(kāi)啟轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。
func setViewControllers(_ viewControllers: [UIViewController], animated animated: Bool)
UITabBarController 下沒(méi)什么特別的:
//注意傳遞的參數(shù)必須是其下的子 VC
unowned(unsafe) var selectedViewController: UIViewController?
var selectedIndex: Int
//和上面類似的整體更新
func setViewControllers(_ viewControllers: [UIViewController]?, animated animated: Bool)
Modal 轉(zhuǎn)場(chǎng):
// Presentation 轉(zhuǎn)場(chǎng)
func presentViewController(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion completion: (() -> Void)?)
// Dismissal 轉(zhuǎn)場(chǎng)
func dismissViewControllerAnimated(_ flag: Bool, completion completion: (() -> Void)?)
Segue
在 storyboard 里設(shè)置 segue有兩種方式:Button to VC,這種在點(diǎn)擊 Button 的時(shí)候觸發(fā)轉(zhuǎn)場(chǎng);VC to VC,這種需要在代碼中調(diào)用performSegueWithIdentifier:sender:。prepareForSegue:sender:方法是在轉(zhuǎn)場(chǎng)發(fā)生前修改轉(zhuǎn)場(chǎng)參數(shù)的最后機(jī)會(huì)。這點(diǎn)對(duì)于 Modal 轉(zhuǎn)場(chǎng)比較重要,因?yàn)樵?storyboard 里 Modal 轉(zhuǎn)場(chǎng)的 Segue 類型不支持選擇 Custom 模式,使用 segue 方式觸發(fā)時(shí)必須在prepareForSegue:sender:里修改模式。
iOS 8 的變化
iOS 8 引入了適應(yīng)性布局,由此添加了兩種新的方式來(lái)顯示一個(gè)視圖控制器:
func showViewController(_ vc: UIViewController, sender sender: AnyObject?)
func showDetailViewController(_ vc: UIViewController, sender sender: AnyObject?)
這兩個(gè)方法咋看上去是給 UISplitViewController 用的,在 storyboard 里 segue 的候選模式里,直接給出了Show(e.g. Push)和Show Detail(e.g. Replace)這樣的提示,以至于我之前一直對(duì)這兩個(gè) segue 有誤解。實(shí)際上這兩個(gè)方法智能判斷當(dāng)前的顯示環(huán)境來(lái)決定如何顯示,iOS 8 想統(tǒng)一顯示視圖控制器的方式,不過(guò)引入這兩個(gè)方法增加了使用的復(fù)雜性,來(lái)看看這兩個(gè)方法的使用規(guī)則。
這兩個(gè)方法在 UISplitViewController 上的確是按名字顯示的那樣去工作的,而在本文關(guān)注的控制器上是這樣工作的:
ViewControllerNavigationControllerTabBarController
showViewController:sender:PresentationPushPresentation(by self)
showDetailViewController:sender:PresentationPresentation(by self)Presentation(by self)
UINavigationController 重寫(xiě)了showViewController:sender:而執(zhí)行 push 操作,上面的by self意思是用容器 VC 本身而非其下子 VC 去執(zhí)行 presentation。這兩個(gè)方法的行為可以通過(guò)重寫(xiě)來(lái)改變。
當(dāng)非容器類 VC 內(nèi)嵌在這兩種容器 VC 里時(shí),會(huì)通過(guò)最近的容器 VC 來(lái)執(zhí)行:
VC in NavigationControllerVC in TabBarController
showViewController:sender:Push(by NavigationController)Presentation(by TabBarController)
showDetailViewController:sender:Presentation(by NavigationController)Presentation(by TabBarController)
前言里從行為上解釋了轉(zhuǎn)場(chǎng),那在轉(zhuǎn)場(chǎng)時(shí)發(fā)生了什么?下圖是從 WWDC 2013 Session 218 整理的,解釋了轉(zhuǎn)場(chǎng)時(shí)視圖控制器和其對(duì)應(yīng)的視圖在結(jié)構(gòu)上的變化:
轉(zhuǎn)場(chǎng)過(guò)程中,作為容器的父 VC 維護(hù)著多個(gè)子 VC,但在視圖結(jié)構(gòu)上,只保留一個(gè)子 VC 的視圖,所以轉(zhuǎn)場(chǎng)的本質(zhì)是下一場(chǎng)景(子 VC)的視圖替換當(dāng)前場(chǎng)景(子 VC)的視圖以及相應(yīng)的控制器(子 VC)的替換,表現(xiàn)為當(dāng)前視圖消失和下一視圖出現(xiàn),基于此進(jìn)行動(dòng)畫(huà),動(dòng)畫(huà)的方式非常多,所以限制最終呈現(xiàn)的效果就只有你的想象力了。圖中的 Parent VC 可替換為 UIViewController, UITabbarController 或 UINavigationController 中的任何一種。
iOS 7 以協(xié)議的方式開(kāi)放了自定義轉(zhuǎn)場(chǎng)的 API,協(xié)議的好處是不再拘泥于具體的某個(gè)類,只要是遵守該協(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à):
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)上面三種類型的轉(zhuǎn)場(chǎng):
//UINavigationController 的 delegate 屬性遵守該協(xié)議。
//UITabBarController 的 delegate 屬性遵守該協(xié)議。
//UIViewController 的 transitioningDelegate 屬性遵守該協(xié)議。
這里除了是 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.動(dòng)畫(huà)控制器(Animation Controller):
最重要的部分,負(fù)責(zé)添加視圖以及執(zhí)行動(dòng)畫(huà);遵守協(xié)議;由我們實(shí)現(xiàn)。
3.交互控制器(Interaction Controller):
通過(guò)交互手段,通常是手勢(shì)來(lái)驅(qū)動(dòng)動(dòng)畫(huà)控制器實(shí)現(xiàn)的動(dòng)畫(huà),使得用戶能夠控制整個(gè)過(guò)程;遵守協(xié)議;系統(tǒng)已經(jīng)打包好現(xiàn)成的類供我們使用。
4.轉(zhuǎn)場(chǎng)環(huán)境(Transition Context):
提供轉(zhuǎn)場(chǎng)中需要的數(shù)據(jù);遵守協(xié)議;由 UIKit 在轉(zhuǎn)場(chǎng)開(kāi)始前生成并提供給我們提交的動(dòng)畫(huà)控制器和交互控制器使用。
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í)候很少用到;遵守協(xié)議;由 UIKit 在轉(zhuǎn)場(chǎng)時(shí)生成,UIViewController 在 iOS 7 中新增了方法transitionCoordinator()返回一個(gè)遵守該協(xié)議的對(duì)象,且該方法只在該控制器處于轉(zhuǎn)場(chǎng)過(guò)程中才返回一個(gè)此類對(duì)象,不參與轉(zhuǎn)場(chǎng)時(shí)返回 nil。
總結(jié)下,5個(gè)協(xié)議只需要我們操心3個(gè);實(shí)現(xiàn)一個(gè)最低限度可用的轉(zhuǎn)場(chǎng)動(dòng)畫(huà),我們只需要提供上面五個(gè)組件里的兩個(gè):轉(zhuǎn)場(chǎng)代理和動(dòng)畫(huà)控制器即可,還有一個(gè)轉(zhuǎn)場(chǎng)環(huán)境是必需的,不過(guò)這由系統(tǒng)提供;當(dāng)進(jìn)一步實(shí)現(xiàn)交互轉(zhuǎn)場(chǎng)時(shí),還需要我們提供交互控制器,也有現(xiàn)成的類供我們使用。
這個(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)效果。動(dòng)畫(huà)控制器是表現(xiàn)轉(zhuǎn)場(chǎng)效果的核心部分,代理部分非常簡(jiǎn)單,我們先搞定動(dòng)畫(huà)控制器吧。
動(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)單,出于篇幅考慮不貼出這個(gè)方法的代碼實(shí)現(xià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è)遵守協(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é)議的對(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)。你可以在 fromView 和 toView 上添加任何動(dòng)畫(huà),轉(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)
轉(zhuǎn)場(chǎng) API 是協(xié)議的好處是不限制具體的類,只要對(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ò)程中使用。
需要舉個(gè)例子了,實(shí)現(xiàn)哪個(gè)好呢?
毫無(wú)疑問(wèn),上面那個(gè)簡(jiǎn)單的。Are you kidding me?這種轉(zhuǎn)場(chǎng)動(dòng)畫(huà)也需要你寫(xiě)這么長(zhǎng)的廢話來(lái)教我怎么實(shí)現(xiàn)?好吧,你要知道轉(zhuǎn)場(chǎng)動(dòng)畫(huà)是轉(zhuǎn)場(chǎng)與動(dòng)畫(huà)的配合,下面更炫酷一點(diǎn)的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)和上面的五毛動(dòng)畫(huà)相比,它們?cè)谵D(zhuǎn)場(chǎng)技術(shù)部分并沒(méi)有什么區(qū)別,主要的差別在動(dòng)畫(huà)的部分。事實(shí)是,不管復(fù)雜與否,所有的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)在實(shí)現(xiàn)轉(zhuǎn)場(chǎng)的部分都沒(méi)有什么差別,而且從技術(shù)上來(lái)講,實(shí)現(xiàn)轉(zhuǎn)場(chǎng)并沒(méi)有高深的東西,如果你動(dòng)手實(shí)現(xiàn)過(guò)幾次,你就能搞定所有的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)中轉(zhuǎn)場(chǎng)的那部分。所以,為了安安靜靜學(xué)習(xí)轉(zhuǎn)場(chǎng)以及省點(diǎn)篇幅,我選擇上面的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)作為例子。
在交互式轉(zhuǎn)場(chǎng)章節(jié)里我們將在上面 Slide 動(dòng)畫(huà)的基礎(chǔ)上實(shí)現(xiàn)文章開(kāi)頭提到的兩種效果:NavigationController 右滑返回 和 TabBarController 滑動(dòng)切換。盡管對(duì)動(dòng)畫(huà)控制器來(lái)說(shuō),轉(zhuǎn)場(chǎng)方式并不重要,可以對(duì) fromView 和 toView 進(jìn)行任何動(dòng)畫(huà),是的,任何動(dòng)畫(huà),但上面的動(dòng)畫(huà)和 Modal 轉(zhuǎn)場(chǎng)風(fēng)格上有點(diǎn)不配,主要?jiǎng)赢?huà)的方向不對(duì),我在這個(gè) Slide 動(dòng)畫(huà)控制器里為 Modal 轉(zhuǎn)場(chǎng)適配了和系統(tǒng)的風(fēng)格類似的豎直移動(dòng)動(dòng)畫(huà)效果;另外 Modal 轉(zhuǎn)場(chǎng)并沒(méi)有比較合乎操作直覺(jué)的交互手段,而且和前面兩種容器控制器的轉(zhuǎn)場(chǎng)在機(jī)制上有些不同,所以我將為 Modal 轉(zhuǎn)場(chǎng)示范另外一個(gè)動(dòng)畫(huà)。
Demo 中的 Slide 動(dòng)畫(huà)控制器適用于三種轉(zhuǎn)場(chǎng),不必修改就可以直接在工程中使用。轉(zhuǎn)場(chǎng)中的操作是可逆的,你可以為了每一種操作實(shí)現(xiàn)單獨(dú)的動(dòng)畫(huà)控制器,也可以實(shí)現(xiàn)通用的動(dòng)畫(huà)控制器。為此,Demo 中的 Slide 動(dòng)畫(huà)控制器針對(duì)轉(zhuǎn)場(chǎng)的操作類型進(jìn)行了適配。Swift 中 enum 的關(guān)聯(lián)值可以視作有限數(shù)據(jù)類型的集合體,在這種場(chǎng)景下極其合適。設(shè)定轉(zhuǎn)場(chǎng)類型:
enum SDETransitionType{
//UINavigationControllerOperation 是枚舉類型,有 None, Push, Pop 三種值。
case NavigationTransition(UINavigationControllerOperation)
case TabTransition(TabOperationDirection)
case ModalTransition(ModalOperation)
}
enum TabOperationDirection{
case Left, Right
}
enum ModalOperation{
case Presentation, Dismissal
}
使用示例:在 TabBarController 中切換到左邊的頁(yè)面。
let transitionType = SDETransitionType.TabTransition(.Left)
Slide 動(dòng)畫(huà)控制器的核心代碼:
class SlideAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
init(type: SDETransitionType) {...}
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ì)述),也不必非得是addSubview:,某些場(chǎng)合你可能需要調(diào)整 fromView 和 toView 的顯示順序,總之將之加入到 containerView 里就行了;
動(dòng)畫(huà)結(jié)束后正確地結(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)的結(jié)果,然后使用completeTransition:來(lái)通知系統(tǒng)轉(zhuǎn)場(chǎng)過(guò)程結(jié)束,這個(gè)方法會(huì)檢查動(dòng)畫(huà)控制器是否實(shí)現(xiàn)了animationEnded:方法,如果有,則調(diào)用該方法。
至此,你已經(jīng)能夠搞定任何動(dòng)畫(huà)控制器中轉(zhuǎn)場(chǎng)的部分了,無(wú)論轉(zhuǎn)場(chǎng)動(dòng)畫(huà)是簡(jiǎn)單的還是超級(jí)復(fù)雜的,是的,就這么簡(jiǎn)單,沒(méi)有任何高深的東西了。轉(zhuǎn)場(chǎng)結(jié)束后,fromView 會(huì)從視圖結(jié)構(gòu)中移除,UIKit 自動(dòng)替我們做了這事,你也可以手動(dòng)處理提前將 fromView 移除,這完全取決于你。雖然這個(gè)動(dòng)畫(huà)控制器實(shí)現(xiàn)的動(dòng)畫(huà)非常簡(jiǎn)單,但此刻我們已經(jīng)替換掉了系統(tǒng)提供的默認(rèn)轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。
以上的代碼是常規(guī)的實(shí)現(xiàn)手法,這里還有另外一條更簡(jiǎn)單的路:UIView的類方法
transitionFromView:toView:duration:options:completion:
甚至不需要獲取 containerView 以及手動(dòng)添加 toView 就能實(shí)現(xiàn)一個(gè)指定類型的轉(zhuǎn)場(chǎng)動(dòng)畫(huà),而缺點(diǎn)則是只能使用指定類型的動(dòng)畫(huà)。
UIView.transitionFromView(fromView, toView: toView, duration: durantion, options: .TransitionCurlDown, completion: { _ in
let isCancelled = transitionContext.transitionWasCancelled()
transitionContext.completeTransition(!isCancelled)
})
看到這里是否想起了點(diǎn)什么?UIViewController用于在子 VC 間轉(zhuǎn)換的方法:
transitionFromViewController:toViewController:duration:options:animations:completion:
該方法用 toVC 的視圖替換 fromVC 的視圖在父視圖中的位置并且執(zhí)行animations閉包里的動(dòng)畫(huà),但這個(gè)方法僅限于在自定義容器控制器里使用,直接使用 UINavigationController 和 UITabBarController 調(diào)用該方法在其下的子 VC 間轉(zhuǎn)換會(huì)拋出異常。不過(guò) iOS 7 中這兩個(gè)容器控制器開(kāi)放的自定義轉(zhuǎn)場(chǎng)做的是同樣的事情,回頭再看第一章Transition 解釋,轉(zhuǎn)場(chǎng)協(xié)議 API 將這個(gè)方法拆分成了上面的幾個(gè)組件,并且加入了激動(dòng)人心的交互控制,以便我們能夠方便定制轉(zhuǎn)場(chǎng)動(dòng)畫(huà)。
事先聲明:盡管 Modal 轉(zhuǎn)場(chǎng)和上面兩種容器 VC 的轉(zhuǎn)場(chǎng)在控制器結(jié)構(gòu)以及視圖結(jié)構(gòu)都有點(diǎn)差別,但是在代碼里實(shí)現(xiàn)轉(zhuǎn)場(chǎng)時(shí),差異非常小,僅有一處地方需要注意。所以,本節(jié)也可以直奔末尾,記住結(jié)論就好。
上一節(jié)里兩種容器 VC 的轉(zhuǎn)場(chǎng)里,fromVC 和 toVC 都是其子 VC,而在 Modal 轉(zhuǎn)場(chǎng)里并非這樣的關(guān)系,fromVC(presentingVC) present toVC(presentedVC),前者為后者提供顯示的環(huán)境。兩類轉(zhuǎn)場(chǎng)的視圖結(jié)構(gòu)差異如下:
轉(zhuǎn)場(chǎng)前后可以在控制臺(tái)打印出它們的視圖控制器結(jié)構(gòu)以及視圖結(jié)構(gòu)觀察變化情況,不熟悉相關(guān)命令的話推薦使用chisel工具,而使用 Xcode 的 ViewDebugging 功能可以直觀地查看應(yīng)用的視圖結(jié)構(gòu)。如果你對(duì)轉(zhuǎn)場(chǎng)中 containerView 這個(gè)角色感興趣,可以通過(guò)上面的方法來(lái)查看。
容器類 VC 的轉(zhuǎn)場(chǎng)里 fromView 和 toView 是 containerView 的子層次的視圖,而 Modal 轉(zhuǎn)場(chǎng)里 presentingView 與 containerView 是同層次的視圖,只有 presentedView 是 containerView 的子層次視圖。
這種視圖結(jié)構(gòu)上的差異與 Modal 轉(zhuǎn)場(chǎng)的另外一個(gè)不同點(diǎn)是相契合的:轉(zhuǎn)場(chǎng)結(jié)束后 fromView 可能依然可見(jiàn),比如 UIModalPresentationPageSheet 模式的 Modal 轉(zhuǎn)場(chǎng)就是這樣。容器 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 模式(以下簡(jiǎn)稱 Custom 模式)下 Modal 轉(zhuǎn)場(chǎng)結(jié)束時(shí) fromView 并未從視圖結(jié)構(gòu)中移除;UIModalPresentationFullScreen 模式(以下簡(jiǎn)稱 FullScreen 模式)的 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)景:
FullScreen 模式:presentation 結(jié)束后,presentingView 被主動(dòng)移出視圖結(jié)構(gòu),不過(guò),在 dismissal 轉(zhuǎn)場(chǎng)中希望其出現(xiàn)在屏幕上并且在對(duì)其添加動(dòng)畫(huà)怎么辦呢?實(shí)際上,你按照容器類 VC 轉(zhuǎn)場(chǎng)里動(dòng)畫(huà)控制器里那樣做也沒(méi)有問(wèn)題,就是將其加入 containerView 并添加動(dòng)畫(huà)。不用擔(dān)心,轉(zhuǎn)場(chǎng)結(jié)束后,UIKit 會(huì)自動(dòng)將其恢復(fù)到原來(lái)的位置。雖然背后的機(jī)制不一樣,但這個(gè)模式下的 Modal 轉(zhuǎn)場(chǎng)和容器類 VC 的轉(zhuǎn)場(chǎng)的動(dòng)畫(huà)控制器的代碼可以通用,你不必記住背后的差異。
Custom 模式:presentation 結(jié)束后,presentingView(fromView) 未被主動(dòng)移出視圖結(jié)構(gòu),在 dismissal 中,注意不要像其他轉(zhuǎn)場(chǎng)中那樣將 presentingView(toView) 加入 containerView 中,否則 dismissal 結(jié)束后本來(lái)可見(jiàn)的 presentingView 將會(huì)隨著 containerView 一起被移除。如果你在 Custom 模式下沒(méi)有注意到這點(diǎn),很容易出現(xiàn)黑屏之類的現(xiàn)象而不知道問(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)上,需要?jiǎng)赢?huà)控制器用一個(gè)變量來(lái)保存 presentingView 的父視圖以便在 dismissal 轉(zhuǎn)場(chǎng)中恢復(fù),第三方的動(dòng)畫(huà)控制器必須為此改造。顯然,這樣的代價(jià)是無(wú)法接受的。為何 FullScreen 模式的 dismissal 轉(zhuǎn)場(chǎng)里就可以任性地將 presentingView 加入到 containerView 里呢?因?yàn)?UIKit 知道 presentingView 的視圖結(jié)構(gòu),即使強(qiáng)行將其從原來(lái)的視圖結(jié)構(gòu)遷移到 containerView,事后將其恢復(fù)到正確的位置也是很容易的事情。
由于以上的區(qū)別導(dǎo)致實(shí)現(xiàn)交互化的時(shí)候在 Custom 模式下無(wú)法控制轉(zhuǎn)場(chǎng)過(guò)程中添加到 presentingView 上面的動(dòng)畫(huà)。解決手段請(qǐng)看特殊的 Modal 轉(zhuǎn)場(chǎng)交互化一節(jié)。
結(jié)論:不要干涉官方對(duì) Modal 轉(zhuǎn)場(chǎng)的處理,我們?nèi)ミm應(yīng)它。在 Custom 模式下的 dismissal 轉(zhuǎn)場(chǎng)中不要像其他的轉(zhuǎn)場(chǎng)那樣將 toView(presentingView) 加入 containerView,否則 presentingView 將消失不見(jiàn),而應(yīng)用則也很可能假死。而 FullScreen 模式下可以使用與前面的容器類 VC 轉(zhuǎn)場(chǎng)同樣的代碼。因此,上一節(jié)里示范的 Slide 動(dòng)畫(huà)控制器不適合在 Custom 模式下使用,放心好了,Demo 里適配好了,具體的處理措施,請(qǐng)看下一節(jié)的處理。
iOS 8 為協(xié)議添加了viewForKey:方法以方便獲取 fromView 和 toView,但是在 Modal 轉(zhuǎn)場(chǎng)里要注意,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。
UIKit 已經(jīng)為 Modal 轉(zhuǎn)場(chǎng)實(shí)現(xiàn)了多種效果,當(dāng) UIViewController 的modalPresentationStyle屬性為.Custom或.FullScreen時(shí),我們就有機(jī)會(huì)定制轉(zhuǎn)場(chǎng)效果,此時(shí)modalTransitionStyle指定的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)將會(huì)被忽略。補(bǔ)充說(shuō)明:自定義 Modal 轉(zhuǎn)場(chǎng)時(shí),modalPresentationStyle屬性也可以為其他值,當(dāng)你提供了轉(zhuǎn)場(chǎng)代理和動(dòng)畫(huà)控制器后,系統(tǒng)就將轉(zhuǎn)場(chǎng)這件事全權(quán)交給你負(fù)責(zé)了,UIKit 內(nèi)部并沒(méi)有對(duì)modalPresentationStyle的值進(jìn)行過(guò)濾,然而該屬性的值不是.Custom或.FullScreen這兩個(gè)官方支持的值時(shí),會(huì)出現(xiàn)各種瑕疵。總之,在探索時(shí)可以各種試探,但是干活時(shí)還是老老實(shí)實(shí)聽(tīng)官方的話。詳細(xì)討論可以查看這個(gè)issue。
Modal 轉(zhuǎn)場(chǎng)開(kāi)放自定義功能后最令人感興趣的是定制 presentedView 的尺寸,下面來(lái)我們來(lái)實(shí)現(xiàn)一個(gè)帶暗色調(diào)背景的小窗口效果。Demo 地址:CustomModalTransition。
由于需要保持 presentingView 可見(jiàn),這里的 Modal 轉(zhuǎn)場(chǎng)應(yīng)該采用 UIModalPresentationCustom 模式,此時(shí) presentedVC 的modalPresentationStyle屬性值應(yīng)設(shè)置為.Custom。而且與容器 VC 的轉(zhuǎn)場(chǎng)的代理由容器 VC 自身的代理提供不同,Modal 轉(zhuǎn)場(chǎng)的代理由 presentedVC 提供。動(dòng)畫(huà)控制器的核心代碼:
class OverlayAnimationController: NSobject, UIViewControllerAnimatedTransitioning{
...
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
...
//不像容器 VC 轉(zhuǎn)場(chǎng)里需要額外的變量來(lái)標(biāo)記操作類型,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)
})
}
}
}
Modal 轉(zhuǎn)場(chǎng)在 Custom 模式下必須區(qū)分 presentation 和 dismissal 轉(zhuǎn)場(chǎng),而在 FullScreen 模式下可以不用這么做,因?yàn)?UIKit 會(huì)在 dismissal 轉(zhuǎn)場(chǎng)結(jié)束后自動(dòng)將 presentingView 放置到原來(lái)的位置。
在 Demo 里,Slide 動(dòng)畫(huà)控制器里適配所有類型的轉(zhuǎn)場(chǎng)是這樣處理的:
switch transitionType{
case .ModalTransition(let operation):
switch operation{
case .Presentation: containerView.addSubview(toView)
case .Dismissal: break
}
default: containerView.addSubview(toView)
}
轉(zhuǎn)場(chǎng)環(huán)境對(duì)象本身也提供了presentationStyle()方法來(lái)查詢 Modal 轉(zhuǎn)場(chǎng)的類型,在一般通用型的動(dòng)畫(huà)控制器里可以這樣處理:
if !(transitionContext.presentationStyle() == .Custom && fromVC.isBeingDismissed()){
containerView.addSubview(toView)
}
前面容器 VC 的轉(zhuǎn)場(chǎng)里提到可以使用UIView的類方法transitionFromView:toView:duration:options:completion:在animateTransition:方法中來(lái)執(zhí)行子視圖的轉(zhuǎn)換,Modal 轉(zhuǎn)場(chǎng)里,fromView 和 toView 并非同一容器視圖下同層次的子視圖,該方法并不適用。不過(guò)經(jīng)測(cè)試,該方法在 Custom 模式下工作正常,F(xiàn)ullScreen 模式有點(diǎn)不兼容。由于在 Modal 轉(zhuǎn)場(chǎng)支持兩種模式,為避免混淆建議不要使用該方法來(lái)轉(zhuǎn)換視圖。
至此,三種主流轉(zhuǎn)場(chǎng)的動(dòng)畫(huà)控制器基本介紹完畢了,可以看到動(dòng)畫(huà)控制器里有關(guān)轉(zhuǎn)場(chǎng)的部分是非常簡(jiǎn)單的,沒(méi)什么難度,也沒(méi)什么高級(jí)的用法,剩下的動(dòng)畫(huà)部分,如前面提到的那樣,你可以為 fromView 和 toView 添加任何動(dòng)畫(huà),而這又是另外一個(gè)話題了。
iOS 8的改進(jìn):UIPresentationController
iOS 8 針對(duì)分辨率日益分裂的 iOS 設(shè)備帶來(lái)了新的適應(yīng)性布局方案,以往有些專為在 iPad 上設(shè)計(jì)的控制器也能在 iPhone 上使用了,一個(gè)大變化是在視圖控制器的(模態(tài))顯示過(guò)程,包括轉(zhuǎn)場(chǎng)過(guò)程,引入了UIPresentationController類,該類接管了 UIViewController 的顯示過(guò)程,為其提供轉(zhuǎn)場(chǎng)和視圖管理支持。在 iOS 8.0 以上的系統(tǒng)里,你可以在 presentation 轉(zhuǎn)場(chǎng)結(jié)束后打印視圖控制器的結(jié)構(gòu),會(huì)發(fā)現(xiàn) presentedVC 是由一個(gè)UIPresentationController對(duì)象來(lái)顯示的,查看視圖結(jié)構(gòu)也能看到 presentedView 是 UIView 私有子類的UITtansitionView的子視圖,這就是前面 containerView 的真面目(劇透了)。
當(dāng) UIViewController 的modalPresentationStyle屬性為.Custom時(shí)(不支持.FullScreen),我們有機(jī)會(huì)通過(guò)控制器的轉(zhuǎn)場(chǎng)代理提供UIPresentationController的子類對(duì) Modal 轉(zhuǎn)場(chǎng)進(jìn)行進(jìn)一步的定制。實(shí)際上該類也可以在.FullScreen模式下使用,但是會(huì)丟失由該類負(fù)責(zé)的動(dòng)畫(huà),保險(xiǎn)起見(jiàn)還是遵循官方的建議,只在.Custom模式下使用該類。官方對(duì)該類參與轉(zhuǎn)場(chǎng)的流程和使用方法有非常詳細(xì)的說(shuō)明:Creating Custom Presentations。
UIPresentationController類主要給 Modal 轉(zhuǎn)場(chǎng)帶來(lái)了以下幾點(diǎn)變化:
定制 presentedView 的外觀:設(shè)定 presentedView 的尺寸以及在 containerView 中添加自定義視圖并為這些視圖添加動(dòng)畫(huà);
可以選擇是否移除 presentingView;
可以在不需要?jiǎng)赢?huà)控制器的情況下單獨(dú)工作;
iOS 8 中的適應(yīng)性布局。
以上變化中第1點(diǎn) iOS 7 中也能做到,3和4是 iOS 8 帶來(lái)的新特性,只有第2點(diǎn)才真正解決了 iOS 7 中的痛點(diǎn)。在 iOS 7 中定制外觀時(shí),動(dòng)畫(huà)控制器需要負(fù)責(zé)管理額外添加的的視圖,UIPresentationController類將該功能剝離了出來(lái)獨(dú)立負(fù)責(zé),其提供了如下的方法參與轉(zhuǎn)場(chǎng),對(duì)轉(zhuǎn)場(chǎng)過(guò)程實(shí)現(xiàn)了更加細(xì)致的控制,從命名便可以看出與動(dòng)畫(huà)控制器里的animateTransition:的關(guān)系:
func presentationTransitionWillBegin()
func presentationTransitionDidEnd(_ completed: Bool)
func dismissalTransitionWillBegin()
func dismissalTransitionDidEnd(_ completed: Bool)
除了 presentingView,UIPresentationController類擁有轉(zhuǎn)場(chǎng)過(guò)程中剩下的角色:
//指定初始化方法。
init(presentedViewController presentedViewController: UIViewController, presentingViewController presentingViewController: UIViewController)
var presentingViewController: UIViewController { get }
var presentedViewController: UIViewController { get }
var containerView: UIView? { get }
//提供給動(dòng)畫(huà)控制器使用的視圖,默認(rèn)返回 presentedVC.view,通過(guò)重寫(xiě)該方法返回其他視圖,但一定要是 presentedVC.view 的上層視圖。
func presentedView() -> UIView?
沒(méi)有 presentingView 是因?yàn)?Custom 模式下 presentingView 不受 containerView 管理,UIPresentationController類并沒(méi)有改變這一點(diǎn)。iOS 8 擴(kuò)充了轉(zhuǎn)場(chǎng)環(huán)境協(xié)議,可以通過(guò)viewForKey:方便獲取轉(zhuǎn)場(chǎng)的視圖,而該方法在 Modal 轉(zhuǎn)場(chǎng)中獲取的是presentedView()返回的視圖。因此我們可以在子類中將 presentedView 包裝在其他視圖后重寫(xiě)該方法返回包裝后的視圖當(dāng)做 presentedView 在動(dòng)畫(huà)控制器中使用。
接下來(lái),我用UIPresentationController子類實(shí)現(xiàn)上一節(jié)「Modal 轉(zhuǎn)場(chǎng)實(shí)踐」里的效果,presentingView 和 presentedView 的動(dòng)畫(huà)由動(dòng)畫(huà)控制器負(fù)責(zé),剩下的事情可以交給我們實(shí)現(xiàn)的子類來(lái)完成。
參與角色都準(zhǔn)備好了,但有個(gè)問(wèn)題,無(wú)法直接訪問(wèn)動(dòng)畫(huà)控制器,不知道轉(zhuǎn)場(chǎng)的持續(xù)時(shí)間,怎么與轉(zhuǎn)場(chǎng)過(guò)程同步?這時(shí)候前面提到的用處甚少的轉(zhuǎn)場(chǎng)協(xié)調(diào)器(Transition Coordinator)將在這里派上用場(chǎng)。該對(duì)象可通過(guò) UIViewController 的transitionCoordinator()方法獲取,這是 iOS 7 為自定義轉(zhuǎn)場(chǎng)新增的 API,該方法只在控制器處于轉(zhuǎn)場(chǎng)過(guò)程中才返回一個(gè)與當(dāng)前轉(zhuǎn)場(chǎng)有關(guān)的有效對(duì)象,其他時(shí)候返回 nil。
轉(zhuǎn)場(chǎng)協(xié)調(diào)器遵守協(xié)議,它含有以下幾個(gè)方法:
//與動(dòng)畫(huà)控制器中的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)同步,執(zhí)行其他動(dòng)畫(huà)
animateAlongsideTransition:completion:
//與動(dòng)畫(huà)控制器中的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)同步,在指定的視圖內(nèi)執(zhí)行動(dòng)畫(huà)
animateAlongsideTransitionInView:animation:completion:
由于轉(zhuǎn)場(chǎng)協(xié)調(diào)器的這種特性,動(dòng)畫(huà)的同步問(wèn)題解決了。
class OverlayPresentationController: UIPresentationController {
let dimmingView = UIView()
//Presentation 轉(zhuǎn)場(chǎng)開(kāi)始前該方法被調(diào)用。
override func presentationTransitionWillBegin() {
self.containerView?.addSubview(dimmingView)
let initialWidth = containerView!.frame.width*2/3, initialHeight = containerView!.frame.height*2/3
self.dimmingView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
self.dimmingView.center = containerView!.center
self.dimmingView.bounds = CGRect(x: 0, y: 0, width: initialWidth , height: initialHeight)
//使用 transitionCoordinator 與轉(zhuǎn)場(chǎng)動(dòng)畫(huà)并行執(zhí)行 dimmingView 的動(dòng)畫(huà)。
presentedViewController.transitionCoordinator()?.animateAlongsideTransition({ _ in
self.dimmingView.bounds = self.containerView!.bounds
}, completion: nil)
}
//Dismissal 轉(zhuǎn)場(chǎng)開(kāi)始前該方法被調(diào)用。添加了 dimmingView 消失的動(dòng)畫(huà),在上一節(jié)中并沒(méi)有添加這個(gè)動(dòng)畫(huà),
//實(shí)際上由于 presentedView 的形變動(dòng)畫(huà),這個(gè)動(dòng)畫(huà)根本不會(huì)被注意到,此處只為示范。
override func dismissalTransitionWillBegin() {
presentedViewController.transitionCoordinator()?.animateAlongsideTransition({ _ in
self.dimmingView.alpha = 0.0
}, completion: nil)
}
}
OverlayPresentationController類接手了 dimmingView 的工作后,需要回到上一節(jié)OverlayAnimationController里把涉及 dimmingView 的部分刪除,然后在 presentedVC 的轉(zhuǎn)場(chǎng)代理屬性transitioningDelegate中提供該類實(shí)例就可以實(shí)現(xiàn)和上一節(jié)同樣的效果。
func presentationControllerForPresentedViewController(_ presented: UIViewController,
presentingViewController presenting: UIViewController,
sourceViewController source: UIViewController) -> UIPresentationController?{
return OverlayPresentationController(presentedViewController: presented, presentingViewController: presenting)
}
在 iOS 7 中,Custom 模式的 Modal 轉(zhuǎn)場(chǎng)里,presentingView 不會(huì)被移除,如果我們要移除它并妥善恢復(fù)會(huì)破壞動(dòng)畫(huà)控制器的獨(dú)立性使得第三方動(dòng)畫(huà)控制器無(wú)法直接使用;在 iOS 8 中,UIPresentationController解決了這點(diǎn),給予了我們選擇的權(quán)力,通過(guò)重寫(xiě)下面的方法來(lái)決定 presentingView 是否在 presentation 轉(zhuǎn)場(chǎng)結(jié)束后被移除:
func shouldRemovePresentersView() -> Bool
返回 true 時(shí),presentation 結(jié)束后 presentingView 被移除,在 dimissal 結(jié)束后 UIKit 會(huì)自動(dòng)將 presentingView 恢復(fù)到原來(lái)的視圖結(jié)構(gòu)中。此時(shí),Custom 模式與 FullScreen 模式下無(wú)異,完全不必理會(huì)前面 dismissal 轉(zhuǎn)場(chǎng)部分的差異了。另外,這個(gè)方法會(huì)在實(shí)現(xiàn)交互控制的 Modal 轉(zhuǎn)場(chǎng)時(shí)起到關(guān)鍵作用,詳情請(qǐng)看交互轉(zhuǎn)場(chǎng)部分。
你可能會(huì)疑惑,除了解決了 iOS 7 中無(wú)法干涉 presentingView 這個(gè)痛點(diǎn)外,還有什么理由值得我們使用UIPresentationController類?除了能與動(dòng)畫(huà)控制器配合,UIPresentationController類也能脫離動(dòng)畫(huà)控制器獨(dú)立工作,在轉(zhuǎn)場(chǎng)代理里我們僅僅提供后者也能對(duì) presentedView 的外觀進(jìn)行定制,缺點(diǎn)是無(wú)法控制 presentedView 的轉(zhuǎn)場(chǎng)動(dòng)畫(huà),因?yàn)檫@是動(dòng)畫(huà)控制器的職責(zé),這種情況下,presentedView 的轉(zhuǎn)場(chǎng)動(dòng)畫(huà)采用的是默認(rèn)的 Slide Up 動(dòng)畫(huà)效果,轉(zhuǎn)場(chǎng)協(xié)調(diào)器實(shí)現(xiàn)的動(dòng)畫(huà)則是采用默認(rèn)的動(dòng)畫(huà)時(shí)間。
iOS 8 帶來(lái)了適應(yīng)性布局,協(xié)議用于響應(yīng)視圖尺寸變化和屏幕旋轉(zhuǎn)事件,之前用于處理屏幕旋轉(zhuǎn)的方法都被廢棄了。UIViewController 和 UIPresentationController 類都遵守該協(xié)議,在 Modal 轉(zhuǎn)場(chǎng)中如果提供了后者,則由后者負(fù)責(zé)前者的尺寸變化和屏幕旋轉(zhuǎn),最終的布局機(jī)會(huì)也在后者里。在OverlayPresentationController中重寫(xiě)以下方法來(lái)調(diào)整視圖布局以及應(yīng)對(duì)屏幕旋轉(zhuǎn):
override func containerViewWillLayoutSubviews() {
self.dimmingView.center = self.containerView!.center
self.dimmingView.bounds = self.containerView!.bounds
let width = self.containerView!.frame.width * 2 / 3, height = self.containerView!.frame.height * 2 / 3
self.presentedView()?.center = self.containerView!.center
self.presentedView()?.bounds = CGRect(x: 0, y: 0, width: width, height: height)
}
完成動(dòng)畫(huà)控制器后,只需要在轉(zhuǎn)場(chǎng)前設(shè)置好轉(zhuǎn)場(chǎng)代理便能實(shí)現(xiàn)動(dòng)畫(huà)控制器中提供的效果。轉(zhuǎn)場(chǎng)代理的實(shí)現(xiàn)也很簡(jiǎn)單,但是在設(shè)置代理時(shí)有不少陷阱,需要注意。
UINavigationControllerDelegate
定制 UINavigationController 這種容器控制器的轉(zhuǎn)場(chǎng)時(shí),很適合實(shí)現(xiàn)一個(gè)子類,自身集轉(zhuǎn)場(chǎng)代理,動(dòng)畫(huà)控制器于一身,也方便使用,不過(guò)這樣做有時(shí)候又限制了它的使用范圍,別人也實(shí)現(xiàn)了自己的子類時(shí)便不能方便使用你的效果,下面的范例采取的是將轉(zhuǎn)場(chǎng)代理封裝成一個(gè)類。
class SDENavigationControllerDelegate: NSObject, UINavigationControllerDelegate {
//在對(duì)象里,實(shí)現(xiàn)該方法提供動(dòng)畫(huà)控制器,返回 nil 則使用系統(tǒng)默認(rèn)的效果。
func navigationController(navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
//使用上一節(jié)實(shí)現(xiàn)的 Slide 動(dòng)畫(huà)控制器,需要提供操作類型信息。
let transitionType = SDETransitionType.NavigationTransition(operation)
return SlideAnimationController(type: transitionType)
}
}
如果你在代碼里為你的控制器里這樣設(shè)置代理:
//錯(cuò)誤的做法,delegate 是弱引用,在離開(kāi)這行代碼所處的方法范圍后,delegate 將重新變?yōu)?nil,然后什么都不會(huì)發(fā)生。
self.navigationController?.delegate = SDENavigationControllerDelegate()
可以使用強(qiáng)引用的變量來(lái)引用新實(shí)例,且不能使用本地變量,在控制器中新增一個(gè)變量來(lái)維持新實(shí)例就可以了。
self.navigationController?.delegate = strongReferenceDelegate
解決了弱引用的問(wèn)題,這行代碼應(yīng)該放在哪里執(zhí)行呢?很多人喜歡在viewDidLoad()做一些配置工作,但在這里設(shè)置無(wú)法保證是有效的,因?yàn)檫@時(shí)候控制器可能尚未進(jìn)入 NavigationController 的控制器棧,self.navigationController返回的可能是 nil;如果是通過(guò)代碼 push 其他控制器,在 push 前設(shè)置即可;prepareForSegue:sender:方法是轉(zhuǎn)場(chǎng)前更改設(shè)置的最后一次機(jī)會(huì),可以在這里設(shè)置;保險(xiǎn)點(diǎn),使用UINavigationController子類,自己作為代理,省去到處設(shè)置的麻煩。
不過(guò),通過(guò)代碼設(shè)置終究顯得很繁瑣且不安全,在 storyboard 里設(shè)置一勞永逸:在控件庫(kù)里拖拽一個(gè) NSObject 對(duì)象到相關(guān)的 UINavigationControler 上,在控制面板里將其類別設(shè)置為SDENavigationControllerDelegate,然后拖拽鼠標(biāo)將其設(shè)置為代理。
最后一步,像往常一樣觸發(fā)轉(zhuǎn)場(chǎng):
self.navigationController?.pushViewController(toVC, animated: true)//or
self.navigationController?.popViewControllerAnimated(true)
Demo 地址:NavigationControllerTransition。
同樣作為容器控制器,UITabBarController 的轉(zhuǎn)場(chǎng)代理和 UINavigationController 類似,通過(guò)類似的方法提供動(dòng)畫(huà)控制器,不過(guò)的代理方法里提供了操作類型,但的代理方法沒(méi)有提供滑動(dòng)的方向信息,需要我們來(lái)獲取滑動(dòng)的方向。
class SDETabBarControllerDelegate: NSObject, UITabBarControllerDelegate {
//在對(duì)象里,實(shí)現(xiàn)該方法提供動(dòng)畫(huà)控制器,返回 nil 則沒(méi)有動(dòng)畫(huà)效果。
func tabBarController(tabBarController: UITabBarController, animationControllerForTransitionFromViewController
fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?{
let fromIndex = tabBarController.viewControllers!.indexOf(fromVC)!
let toIndex = tabBarController.viewControllers!.indexOf(toVC)!
let tabChangeDirection: TabOperationDirection = toIndex < fromIndex ? .Left : .Right
let transitionType = SDETransitionType.TabTransition(tabChangeDirection)
let slideAnimationController = SlideAnimationController(type: transitionType)
return slideAnimationController
}
}
為 UITabBarController 設(shè)置代理的方法和陷阱與上面的 UINavigationController 類似,注意delegate屬性的弱引用問(wèn)題。點(diǎn)擊 TabBar 的相鄰頁(yè)面進(jìn)行切換時(shí),將會(huì)看到 Slide 動(dòng)畫(huà);通過(guò)以下代碼觸發(fā)轉(zhuǎn)場(chǎng)時(shí)也將看到同樣的效果:
tabBarVC.selectedIndex = ...//or
tabBarVC.selectedViewController = ...
Demo 地址:ScrollTabBarController。
UIViewControllerTransitioningDelegate
Modal 轉(zhuǎn)場(chǎng)的代理協(xié)議是 iOS 7 新增的,其為 presentation 和 dismissal 轉(zhuǎn)場(chǎng)分別提供了動(dòng)畫(huà)控制器。前面實(shí)現(xiàn)的OverlayAnimationController類可同時(shí)處理 presentation 和 dismissal 轉(zhuǎn)場(chǎng)。UIPresentationController只在 iOS 8中可用,通過(guò)available關(guān)鍵字可以解決 API 的版本差異。
class SDEModalTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate {
func animationControllerForPresentedController(presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return OverlayAnimationController()
}
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return OverlayAnimationController()
}
@available(iOS 8.0, *)
func presentationControllerForPresentedViewController(presented: UIViewController,
presentingViewController presenting: UIViewController,
sourceViewController source: UIViewController) -> UIPresentationController? {
return OverlayPresentationController(presentedViewController: presented, presentingViewController: presenting)
}
}
Modal 轉(zhuǎn)場(chǎng)的代理由 presentedVC 的transitioningDelegate屬性來(lái)提供,這與前兩種容器控制器的轉(zhuǎn)場(chǎng)不一樣,不過(guò)該屬性作為代理同樣是弱引用,記得和前面一樣需要有強(qiáng)引用的變量來(lái)維護(hù)該代理,而 Modal 轉(zhuǎn)場(chǎng)需要 presentedVC 來(lái)提供轉(zhuǎn)場(chǎng)代理的特性使得 presentedVC 自身非常適合作為自己的轉(zhuǎn)場(chǎng)代理。另外,需要將 presentedVC 的modalPresentationStyle屬性設(shè)置為.Custom或.FullScreen,只有這兩種模式下才支持自定義轉(zhuǎn)場(chǎng),該屬性默認(rèn)值為.FullScreen。自定義轉(zhuǎn)場(chǎng)時(shí),決定轉(zhuǎn)場(chǎng)動(dòng)畫(huà)效果的modalTransitionStyle屬性將被忽略。
開(kāi)啟轉(zhuǎn)場(chǎng)動(dòng)畫(huà)的方式依然是兩種:在 storyboard 里設(shè)置 segue 并開(kāi)啟動(dòng)畫(huà),但這里并不支持.Custom模式,不過(guò)還有機(jī)會(huì)挽救,轉(zhuǎn)場(chǎng)前的最后一個(gè)環(huán)節(jié)prepareForSegue:sender:方法里可以動(dòng)態(tài)修改modalPresentationStyle屬性;或者全部在代碼里設(shè)置,示例如下:
let presentedVC = ...
presentedVC.transitioningDelegate = strongReferenceSDEModalTransitionDelegate
//當(dāng)與 UIPresentationController 配合時(shí)該屬性必須為.Custom。
presentedVC.modalPresentationStyle = .Custom/.FullScreen
presentingVC.presentViewController(presentedVC, animated: true, completion: nil)