(原文:A Beginner’s Guide to Animated Custom Segues in iOS 8 作者:gabriel theodoropoulos 譯者:ztdj121)
iOS 5發布的時候,蘋果針對應用程序界面的設計,提出了一種全新的,革命性的方法—Storyboard,它從根本上改變了現有的設計理念。iOS 5之前,每個視圖控制器通常都伴有一個Interface Builder的文件,叫nib或者xib,這個想法比較簡單:每個視圖控制器的界面應該在各自的nib文件中設計,而所有的nib文件一起構成了整個應用程序的界面。一方面,這個是很方便的,因為它強迫開發者在界面設計的時候將注意力集中在界面上,但另一方面,到最後,太多的文件不得不被創建,開發者將不能概覽應用的整體界面。
隨著storyboard的產生,上面的這些都成為了歷史,因為這種新方法受到了開發者社區的廣泛使用。相比老的技術,storyboards提供了三個重要的優勢:
整個界面設計只發生在一個文件裡。項目的總文件數量大大減少了,特別是在大項目裡。當然你可以使用額外的nib文件,並且允許只創建輔助視圖。
開發者能即時浏覽應用的界面和流程。
視圖控制器之間的轉換(界面設計的專業術語叫場景(scene)),以及轉換是如何發生的,在storyboard中已被完美地定義並清楚地呈現給了開發者。
綜上所述,場景之間的轉換構成storyboard的特殊部分,我們一般把它叫做轉場(segue)。
轉場跟應用的導航和處理是密切相關的,因為它明確定義了一個視圖切換到另一個視圖的轉換細節。這些細節指定了是否應用動畫,動畫的類型,當然還有實際轉換時的准備和性能。除此之外,轉場也用來將傳遞數據到下一個視圖控制器裡,這個用法也很常見。
從編程的角度看,場景是UIStoryboardSegue類的一個對象,它第一次在iOS 5中介紹到。和其它類的對象不同的是,這種對象不能直接的創建或使用。不過你可以指定轉場的屬性,然後在轉場即將發生時提供給它以達到目的。UIKit框架提供了一些帶默認動畫過渡的預定義的轉場,包括:push segues(包括導航控制器的app),帶有動畫選擇的模態轉場(modal segues), popover segues。更高級的情況下,iOS SDK默認的轉場可能不夠用,所以開發者必須實現他們的自定義轉場(custom segues)。
創建一個自定義轉場並不難,因為它是iOS標准編程技術的組成部分。實際上你只需要生成UIStoryboardSegue的子類,並重載一個叫perform的方法即可。這個perform方法中必須實現自定義動畫的邏輯。從一個視圖控制器轉換到另一個以及返回操作的觸發,也需要由開發者編程提供,這是一個標准的步驟。
在本教程中,我的目標是向你們展示如何實現自定義轉場,並通過一個簡單的演示應用介紹這個概念的所有方面。擁有創建自定義轉場的知識, 可以將你導向開發更強大的app的道路。此外,對於最大化用戶體驗,並開發引人注目的漂亮應用,自定義轉場也很有幫助。
如果你有興趣學習我剛剛說的話,就一起來探索教程裡的所有細節和自定義轉場的奧秘吧。
不像我之前幾個教程提供了一個啟動項目,本教程我們將從頭開始創建app。事實上,我是故意這麼做的,因為,項目中一些重要部分需要用到Interface Builder,所以我認為從頭開始按部就班的來做,能讓你看清裡面的細節。
正如我先前所說,我們將開發一個非常簡單的app,在這個應用中我們將創建兩個自定義轉場。需要提前說明的是,我們的演示應用將有三個視圖控制器,也就是在Interface Builder中有三個場景和三個相關類。默認情況下,第一個是由Xcode創建的,因此我們只要再添加兩個。我們將創建的自定義轉場用來導航第一個視圖控制器到第二個(以及返回),以及從第一個到第三個(以及返回)。第二個和第三個視圖控制器之間我們不添加任何聯系。
因此,我們需要創建兩個自定義轉場。因為要包括返回,每一個轉場需要創建兩個對應的類(因此,共四個):第一個類裡我們將實現從第一個視圖控制器到另一個轉換的所有自定義邏輯。第二個類實現返回到第一個視圖控制器的邏輯,或者換句話說要實現解除轉場(unwind segue)。後面會講到解除轉場,現在只需要記住這就是用來讓我們返回到前一個視圖控制器的轉場。
視圖控制器本身沒什麼需要做的。我們會用一個label注明視圖控制器的名稱,每一個會有一個不同的背景顏色,可以讓我們很容易地查看轉換(是的,這將是一個五顏六色的應用)。第一個和第二個視圖控制器也會多一個label,其中從其他視圖控制器傳來的自定義的消息將被顯示出來。
最後,轉場將在以下的動作發生的時候被觸發:
從第一個視圖控制器轉換到第二個,我們將使用向上滑動的手勢(swipe up).而返回,使用向下滑動的手勢(swipe down)。我不打算描述我們實現的動畫,我會展示給你們看。
從第一個視圖控制器轉換到第三個,我們用一個按鈕(UIButton)。而返回,使用向上滑動手勢。對於動畫不多做說明。
當然,我們也會在視圖控制器之間傳遞數據。
在我們繼續之前,有個最終demo,將展示我們將要做的,以及我們的自定義轉場是如何工作的。
注意,創建一個自定義轉場的時候,你可以實現任何一個你想到的或者需要的動畫。我們做的時候,你可以嘗試做下。
我們開始啟動Xcode,選擇創建一個新工程。在出現的向導中,選擇Single View Application作為工程的模板。選擇下一步,在Product Name字段裡,設置CustomSegues作為項目名稱。同時,確保選擇的編程語言是Swift,設備是iPhone。
選擇下一步。這步中,選擇保存項目的路徑,然後點擊生成按鈕。
現在工程已經准備好了,選擇工程導航對應的組。默認情況下,General選項卡是打開的,這也是我們需要的。在Deployment Info部分,不選擇Device Orientation區域裡的Landscape Left 和 Landscape Right選項,只選擇Portrait選項。如果你要在一個iOS 8之前的設備上測試,自由改變從8.1到之前的任意iOS版本deployment target。(最好不要運行在iOS 8之前的設備上)
現在,我們准備好繼續進行實現,通過第一個界面創建開始。
第一步是添加我們在界面裡要用到的視圖控制器。所以,在工程導航欄(Project Navigator)裡點擊Main.storyboard文件,讓Interface Builder顯示出來。在三個不同的部分,我們將打破我們的工作,每一個裡我們將設置一個視圖控制器。但首先,要確保改變當前的尺寸類到Compact Width 和Regular Height,這樣界面適配iPhone的尺寸。
當打開Interface Builder的時候,你會看到Xcode默認添加的場景。這就是我們開始的地方。在這個場景裡添加兩個UILabel對象。第一個設置屬性如下:
Frame: X=16, Y=77, W=368, H=21
Font: System Bold
Font size: 24
Text: View Controller #1
Text alignment: Center
同時,設置上下左右的約束條件:
給第二個label,設置如下屬性:
Frame: X=16, Y=390, W=368, H=21
Text: None
Text alignment: Center
然後設它的Horizontal Center in Container, Vertical Center in Container),寬和高的約束條件。
完成兩個label的屬性和約束條件設置之後,在場景中添加一個UIButton對象。參數設置如下:
Frame: X=169, Y=712, W=62, H=30
Title: Tap Me!
Text Color: Black
關於它的約束條件,簡單地點擊Interface Builder右下角的大頭針按鈕,選擇Selected Views部分裡的Add Missing Contrains選項。
你完成之後,選擇視圖,改變背景顏色,打開Attributes Inspector裡對應的下拉菜單,選擇“Other…”選項。在顏色選擇器裡,點擊調色板按鈕然後選擇蠟筆*調色板(因為比較柔和一點),關閉顏色選擇器。
下面是第一個場景的截圖:
現在關注第二個視圖控制器。在畫布上直接從庫裡拖一個新的控制器對象。然後也添加兩個UILabel對象,並設置和上面一樣的屬性。別忘了設置約束條件。這邊唯一不同的地方是第一個label的文本,改為“View Controller #2!”(不帶引號)。
下一步,選擇視圖,再次打開顏色選擇器,這次選擇Honeydew(列表中第二個),然後關閉顏色選擇器。
這時,Xcode將給你個警告,因為兩個視圖控制器之間沒有轉場(segue)。沒關系,我們馬上就要解決這個問題了。
下面這個圖展示了第二個場景:
最後,我們從對象庫裡再添加一個視圖控制器到畫布上。這次,只添加一個UILabel對象,不是兩個。設置和前兩個場景中第一個label一樣的屬性。有兩個不同點:第一,設置label的framde的Y值為389;第二,設置文本為“View Controller #3!”。這個視圖裡沒有其他子視圖了,你只要選擇一個背景顏色。和之前兩次一樣,在顏色選擇器裡選擇sky顏色。
對於它的約束條件,和之前兩個視圖控制器裡的第二個label一樣設置 Horizontal Center in Container, Vertical Center in Container,以及寬和高。設置Y值和約束條件,讓label正好顯示在視圖中間。
界面中第三個場景看起來是這樣的:
我們之前添加到界面上的每一個場景,必須設置一個不同的類來實現每一個細節。默認情況下,ViewController設定為每個場景的類,這非常適合我們的第一個視圖控制器(這個是Xcode自動創建的)。而其他兩個,我們需要創建兩個新類,下面就讓我們來實現一下。
第一步是創建新的類文件,到Xcode菜單裡File>New>File…,啟動這個過程,引導創建新文件的細節出現了,所以第一步要確保選擇iOS下,Source類別裡的Cocoa Touch Class。
點擊下一步按鈕。在向導的第二步,在Subclass of裡:字段選擇或輸入UIViewController的值。然後指定SecondViewController作為你剛剛創建的名字。當然你不能改變編程語言,確保最後字段裡選擇的是Swift語言。
點擊下一步,生成新的文件。一旦完成,你會看到一個叫SecondViewController.swift的新文件,出現在Project導航器裡。
重復上述過程,給第三個視圖控制器添加類文件。類名設置為ThirdViewController,剩下的步驟都一樣。
添加完第二個文件後,你應該能在Project導航器裡看到這兩個文件:
我們回到Interface Builder和新創建場景的類之前,讓我們先聲明兩個後面要用到的IBOutlet的屬性,在ViewController.swift裡添加如下屬性:
@IBOutlet?weak?var?lblMessage:?UILabel!
在SecondViewController.swift文件裡,添加和上面完全一樣的設置。這兩個屬性被連接到開始的兩個試圖控制器裡的第二個label上,稍後我們可以顯示一條消息給它們。
現在再次打開Main.storyboard文件,選擇第二個場景(綠色背景那個)。這個上面點擊視圖控制器對象,進到工具面板裡的Identity inspector裡,在Custom Class部分,具體在Class字段,設置我們在工程開始時候添加的第一個類:SecondViewController:
第三個場景(藍色背景的)也一樣的處理。選擇後,點擊視圖控制器的上面,設置ThirdViewController作為它的類:
好了,現在每個場景匹配了一個不同的類。在這部分的最後,我們連接兩個之前聲明的IBOutlet屬性,從第一個視圖控制器(橙色背景的那個)開始。點擊視圖控制器對象的上面,按住Ctrl拖向場景中的那個label:
在彈出的小窗口裡,選擇lblMessage屬性,這樣聯系就完成了。
在SecondViewController場景裡一樣的步驟,關聯那個視圖控制器的lblMessage屬性到場景中的label上。
自定義轉場的創建包括UIStoryboardSegue的子類化,以及一個必須實現的perform方法。方法中,視圖控制器轉換的自定義邏輯實際是在這裡應用,所以,在寫轉場相關的代碼之前,讓我們先添加所有必要的類。
工程中添加新類的方法之前已經詳細介紹過了,所以你需要添加文件的時候也可以將它當成指南。在我給你們每個文件的細節之前,我們一共要創建4個文件。因為每個自定義轉場需要兩個類,我們將創建兩個文件。具體地說,第一個類用來實現所有的邏輯,以及轉場執行的轉換動畫。第二個類用來實現從第二個視圖控制器到第一個的相反動作。也就是所謂的解除轉場(unwind segue),我們稍後將詳細介紹。
綜上所述,開始向項目中添加新類。添加每個文件時注意兩個問題:總是選擇Cocoa Touch Class模板,以及向導中第二步的字段,設置Subclass of的值為UIStoryboardSegue。
我們將四個文件的名字命名為:
FirstCustomSegue
FirstCustomSegueUnwind
SecondCustomSegue
SecondCustomSegueUnwind
這四個文件加完後都顯示在Project導航器裡了。
現在讓我們開始第一個自定義轉場。寫代碼之前,必須在Interface Builder裡添加轉場,給我們想應用自定義轉換的地方聯系兩個場景。本例中,我們將創建的轉場是從ViewController場景(橙色背景)到SecondViewController場景(綠色背景)。
再次打開Main.storyboard,簡單地創建新轉場(Segue),擴大文檔大綱,如果它被隱藏的話,選擇ViewController場景裡的ViewController對象:
然後,按住Ctrl將SecondViewController拖向下面的SecondViewController場景中:
關於新的轉場,彈出個有很多選項的黑色彈出框。其中有個叫custom:
點擊它,轉場就立馬建立了。你可以通過檢查畫布中是否添加了一根連接兩個場景的連線進行驗證。
我們開始寫代碼之前還有些設置要完成。首先,點擊選擇畫布上的轉場,打開Utilities面板裡的屬性檢查器(Attributes inspector)。我們要做得最重要的一個任務是,設置轉場的identifier值,在代碼中會用到這個以便我們引用。真正重要的是不要給多個轉場指定相同的標示符。所以,本例中,設置idFirstSegue為轉場的標示符。同時在Segue Class字段裡,必須指定我們用來執行自定義轉換的自定義類。這裡,我們設置FirstCustomSegue為自定義轉場的類。運行的時候,程序會執行這個類裡存在的代碼,讓轉場正常執行。
現在新的自定義轉場有了,它的設置也指定好了,接下來讓我們實現第一個視圖控制器到第二個控制器的理想的轉換。簡而言之,寫關於自定義轉場的代碼通常要做的是,“玩”兩個視圖控制器的視圖。首先,目標視圖控制器的視圖的初始狀態是被指定的,並且被手動添加到應用的窗口上。然後使用動畫目標視圖被放到想要放的最終位置,源視圖控制離開屏幕的時候也是動畫的。
注意:轉場對象是指將被當做目標視圖控制器呈現的視圖控制器,而當前的視圖控制器就是源視圖控制器(source view controller)。
最後,當動畫轉換結束的時候,新的視圖控制器將直接呈現給用戶,沒有更多的動畫。
app概述部分的教程中,你已經看到了我們想要達到的效果。不管轉場怎麼執行,我們希望第一個視圖控制器的視圖是向上滑動,第二個視圖控制器的視圖跟著動。兩個視圖之間沒有空隙,第二個把第一個給“粘住”了。
現在讓我們開始寫代碼,我們一步一步的看。最後,我會一起提供以下方法的所有實現。
打開FirstCustomSegue.swift文件,添加以下方法定義:
override?func?perform()?{ }
這個方法已經在UIStoryboardSegue的父類裡定義了,這裡我們重載這個方法,以便我們添加想要的自定義邏輯。所以,第一步是使我們的工作更簡單,它指定視圖的源和目標視圖控制器的兩個局部變量,如下所示:
override?func?perform()?{ ????//?Assign?the?source?and?destination?views?to?local?variables. ????var?firstVCView?=?self.sourceViewController.view?as?UIView! ????var?secondVCView?=?self.destinationViewController.view?as?UIView! }
另外,我們需要屏幕的寬和高,然後放在兩個變量裡:
override?func?perform()?{ ????... ????//?Get?the?screen?width?and?height. ????let?screenWidth?=?UIScreen.mainScreen().bounds.size.width ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height }
好了,現在指定應該出現的視圖的初始位置。考慮到我們想要視圖從底部移動到上面,我們把視圖放在屏幕可視區域的外面,當前視圖的正下方。這很簡單,我們簡單地設置下視圖的frame:
override?func?perform()?{ ????... ????//?Specify?the?initial?position?of?the?destination?view. ????secondVCView.frame?=?CGRectMake(0.0,?screenHeight,?screenWidth,?screenHeight) }
這時,第二個視圖控制器的視圖還不是窗口的子視圖。所以,在我們實現真正的動畫之前,很明顯,我們必須把它加到窗口上。這用窗口對象的方法insertSubview(view:aboveSubview:)來實現。正如你接下來看到的,我們先訪問窗口對象,然後添加目標視圖:
override?func?perform()?{ ????... ????//?Access?the?app's?key?window?and?insert?the?destination?view?above?the?current?(source)?one. ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(secondVCView,?aboveSubview:?firstVCView) }
別被上面的話給搞糊塗了。關於窗口子視圖,他們的順序看起來像在棧裡一樣。一般而言它不會帶來麻煩。
最後,讓我們給轉換過程添加動畫。首先,我們將第一個視圖控制器的視圖從屏幕上方移出邊緣,同時,把第二個視圖放到第一個的位置上。實際上,所謂的“移動”,就是修改兩個視圖的frame:
override?func?perform()?{ ????... ????//?Animate?the?transition. ????UIView.animateWithDuration(0.4,?animations:?{?()?->?Void?in???????? ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?-screenHeight) ????????secondVCView.frame?=?CGRectOffset(secondVCView.frame,?0.0,?-screenHeight) ????????})?{?(Finished)?->?Void?in ????} }
上面兩行代碼將執行預期的效果。然而,我們還沒完全結束,因為我們還沒顯示出新的視圖控制器(目標視圖控制器)。我們將在第二個閉包裡實現:
override?func?perform()?{ ????... ????//?Animate?the?transition. ????UIView.animateWithDuration(0.4,?animations:?{?()?->?Void?in ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?-screenHeight) ????????secondVCView.frame?=?CGRectOffset(secondVCView.frame,?0.0,?-screenHeight) ????????})?{?(Finished)?->?Void?in ????????????self.sourceViewController.presentViewController(self.destinationViewController?as?UIViewController, ????????????????animated:?false, ????????????????completion:?nil) ????} }
動畫部分到這裡就結束了。總結一下:
override?func?perform()?{ ????//?Assign?the?source?and?destination?views?to?local?variables. ????var?firstVCView?=?self.sourceViewController.view?as?UIView! ????var?secondVCView?=?self.destinationViewController.view?as?UIView! ????//?Get?the?screen?width?and?height. ????let?screenWidth?=?UIScreen.mainScreen().bounds.size.width ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height ????//?Specify?the?initial?position?of?the?destination?view. ????secondVCView.frame?=?CGRectMake(0.0,?screenHeight,?screenWidth,?screenHeight) ????//?Access?the?app's?key?window?and?insert?the?destination?view?above?the?current?(source)?one. ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(secondVCView,?aboveSubview:?firstVCView) ????//?Animate?the?transition. ????UIView.animateWithDuration(0.4,?animations:?{?()?->?Void?in ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?-screenHeight) ????????secondVCView.frame?=?CGRectOffset(secondVCView.frame,?0.0,?-screenHeight) ????????})?{?(Finished)?->?Void?in ????????????self.sourceViewController.presentViewController(self.destinationViewController?as?UIViewController, ????????????????animated:?false, ????????????????completion:?nil) ????} }
在你給app第一次調試之前,我們還必須執行自定義轉場。這個發生在ViewController視圖控制器的視圖上做向上滑動的手勢。所以,繼續打開ViewController.swift文件。在viewDidLoad方法裡創建一個新的手勢對象,然後把它加到視圖裡:
override?func?viewDidLoad()?{ ????... ????var?swipeGestureRecognizer:?UISwipeGestureRecognizer?=?UISwipeGestureRecognizer(target:?self,?action:?"showSecondViewController") ????swipeGestureRecognizer.direction?=?UISwipeGestureRecognizerDirection.Up ????self.view.addGestureRecognizer(swipeGestureRecognizer) }
當手勢被執行的時候,showSecondViewController方法應該是被調用的,但還沒實現。因此,讓我們添加它的定義,這裡我們將添加簡單的一行代碼來執行自定義轉場:
func?showSecondViewController()?{ ????self.performSegueWithIdentifier("idFirstSegue",?sender:?self) }
可以注意到,我們是通過標示符來訪問轉場的。當我們滑動到視圖,以上的調用使我們在FirstCustomSegue類裡的自定義實現得以執行。
你現在可以調試app了。你可以在模擬器上運行,也可以在真機上運行。不過,即使你可以向上滑動來轉換視圖,你不能返回到第一個視圖控制器。這是正常的,因為我們還沒有定義,我們接下來將實現這個方法。
創建自定義轉場的時候,需要同時實現導航裡的兩個方法:從源視圖控制器到目標視圖控制器,然後返回。幾乎不可能只建立一個方法。現在我們已經實現了轉場,所以它將按我們想要的動畫方式顯示第二個視圖控制器,我們必須讓app能夠做相反的事情。執行返回導航的轉場,叫解除轉場(unwind segue)。
簡而言之,解除轉場和正常的一樣,但是它的配置有點復雜。某些步驟還是要完成的,但沒有超出標准編程技術之外的東西。注意兩點:
解除轉場(unwind segue)通常和正常自定義轉場(segue)一起出現。
要解除轉場起作用,我們必須重寫perform方法,並應用自定義邏輯和我們想要的或者應用規定的動畫。另外,導航返回源視圖控制器的過渡效果不需要和對應的正常轉場相同。
解除轉場的實現分四個步驟:
IBAction方法的創建,該方法在解除轉場被執行的時候會選擇地執行一些代碼。這個方法可以有你想要的任何名字,而且不強制包含其它東西。它需要定義,但可以留空,解除轉場的定義需要依賴這個方法。
解除轉場的創建,設置的配置。這和之前我們在Interface Builder裡的轉場不太一樣,等下我們將看看這個是怎麼實現的。
通過重寫UIStoryboardSegue子類裡的perform方法,來實現一般的邏輯。
UIViewController類提供了特定方法的定義,所以系統知道解除轉場即將執行。
現在你可以會有些迷糊,接下來,我們會解釋所有的東西。
先從上面提到的第一個步驟裡的IBAction方法開始。這個方法通常在我們想要導航的初始視圖控制器裡實現。如我所說,方法裡可以是空的,但是你得記住如果你不定義至少一個這樣的方法,你將無法繼續。當解除轉場存在在app中,沒必要使用那麼多的方法。通常情況下,簡單地定義一個這樣的動作方法,檢測對應的解除轉場對應然後調用,就足夠了。
現在行動,打開ViewController.swift文件,添加如下代碼:
@IBAction?func?returnFromSegueActions(sender:?UIStoryboardSegue){ }
我們將稍後添加些示例代碼。注意這個方法的參數是一個UIStoryboardSegue對象,這個是需要記住的細節。
現在讓我們去Interface Builder裡,打開Main.storyboard文件。是時候創建解除轉場了。我們開始檢查SecondViewController場景裡包含的對象,甚至查看場景本身,或者文檔大綱。如果你仔細觀察,你會發現一個對象,叫Exit:
如果你還沒創建過自定義轉場,很可能你不會用到它,可能會想知道這個干什麼的。這個就是你需要創建的解除轉場。
點擊場景裡SecondViewController對象,按Ctrl拖到Exit對象:
下面的彈出框會顯示我們早前定義的IBAction方法:
確保選中它。解除轉場將被創建,你可以在文檔大綱裡查看到。現在新的聯線被創建了,但轉場在這裡:
現在點擊我們剛創建的解除轉場,打開屬性檢查器。設置“idFirstSegueUnwind”作為轉場的標示符:
解除轉場的第二部分目前已經完成了。接下來我們要開始寫代碼了,並確定解除轉場的執行方法。
首先,打開FirstCustomSegueUnWind.swift文件,再次重寫perform方法,並保持對視圖的局部變量的引用。同時,我們也要在一個變量中保存屏幕高度:
override?func?perform()?{ ????//?Assign?the?source?and?destination?views?to?local?variables. ????var?secondVCView?=?self.sourceViewController.view?as?UIView! ????var?firstVCView?=?self.destinationViewController.view?as?UIView! ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height }
注意,現在源視圖控制器是SecondViewController,目標視圖控制器是ViewController。繼續,這裡我們不指定任何視圖的初始狀態,我們只把第一個視圖控制的視圖(我們想要返回的那個)加到窗口的子視圖上:
override?func?perform()?{ ????... ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(firstVCView,?aboveSubview:?secondVCView) }
最後,我們必須激活兩個視圖的運動。這次,他們都往底部移動:
override?func?perform()?{ ????... ????//?Animate?the?transition. ????UIView.animateWithDuration(0.4,?animations:?{?()?->?Void?in???????? ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?screenHeight) ????????secondVCView.frame?=?CGRectOffset(secondVCView.frame,?0.0,?screenHeight) ????????})?{?(Finished)?->?Void?in ????????????self.sourceViewController.dismissViewControllerAnimated(false,?completion:?nil) ????} }
通過改變兩個視圖的y值,我們設法把他們在預定義動畫的時間內放到新的位置上。當轉換結束,我們只要讓第二個視圖控制器直接消失,不帶任何動畫,然後就結束了。下面一起給你這個方法的實現:
override?func?perform()?{ ????//?Assign?the?source?and?destination?views?to?local?variables. ????var?secondVCView?=?self.sourceViewController.view?as?UIView! ????var?firstVCView?=?self.destinationViewController.view?as?UIView! ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(firstVCView,?aboveSubview:?secondVCView) ????//?Animate?the?transition. ????UIView.animateWithDuration(0.4,?animations:?{?()?->?Void?in ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?screenHeight) ????????secondVCView.frame?=?CGRectOffset(secondVCView.frame,?0.0,?screenHeight) ????????})?{?(Finished)?->?Void?in ????????????self.sourceViewController.dismissViewControllerAnimated(false,?completion:?nil) ????} }
目前為止還好,我們成功創建了初始化要求的IBAction方法和解除轉場,實現了動畫轉換執行的自定義邏輯。現在,還剩最後一個標准方法需要實現,那個方法裡,我們會使用FirstCustomSegueUnwind類。
打開ViewController.swift文件,定義一下方法:
override?func?segueForUnwindingToViewController(toViewController:?UIViewController,?fromViewController:?UIViewController,?identifier:?String?)?->?UIStoryboardSegue?{ }
這是UIViewController類提供的方法。在解除轉場執行的時候會自動調用這個方法,其實現是手動添加的。有三個參數:
fromViewController: 當前顯示的視圖控制器,是我們想讓它消失的。
toViewController: 目標視圖控制器,或者換句話說是我們想顯示的控制器。
identifier: 將被執行的轉場的標示符。
注意,這個方法返回的是一個轉場對象。我們要做的很簡單:我們會用identifier的值來決定要執行的那個轉場,然後初始化一個解除轉場自定義類的對象,這個類對象是我們最後要返回的。同時,調用一個super的方法,在這裡重載下,讓我們看看代碼:
override?func?segueForUnwindingToViewController(toViewController:?UIViewController,?fromViewController:?UIViewController,?identifier:?String?)?->?UIStoryboardSegue?{ ????if?let?id?=?identifier{ ????????if?id?==?"idFirstSegueUnwind"?{ ????????????let?unwindSegue?=?FirstCustomSegueUnwind(identifier:?id,?source:?fromViewController,?destination:?toViewController,?performHandler:?{?()?->?Void?in ????????????}) ????????????return?unwindSegue ????????}???????? ????} ????return?super.segueForUnwindingToViewController(toViewController,?fromViewController:?fromViewController,?identifier:?identifier) }
首先,我們要確認identifier參數有實際的值,所以用if let語句。然後用idFirstSegueUnwind值來確認這個轉場是我們想要的,在if語句中初始化FirstCustomSegueUnwind類對象。這個對象在if語句中返回,外面我們返回調用super類之後同一個方法返回的值。
現在解除轉場准備好了。剩下實現的是觸發的它的手勢。打開SecondViewController.swift文件,直接到viewDidLoad方法裡。添加如下代碼:
override?func?viewDidLoad()?{ ????... ????var?swipeGestureRecognizer:?UISwipeGestureRecognizer?=?UISwipeGestureRecognizer(target:?self,?action:?"showFirstViewController") ????swipeGestureRecognizer.direction?=?UISwipeGestureRecognizerDirection.Down ????self.view.addGestureRecognizer(swipeGestureRecognizer) }
上面定義的手勢是向下滑動的手勢,剛好與之前的相反。
現在定義showFirstViewController方法:
func?showFirstViewController()?{ ????self.performSegueWithIdentifier("idFirstSegueUnwind",?sender:?self) }
現在一切都准備好了,你可以再次調試app。這次,你可以在視圖控制器之間來回導航。
前面的部分,我們聲明了一個空方法:returnFromSegueActions(sender:), 我們說這個方法是必須的,這樣我們才能創建解除轉場。同時,我也說過後面會添加一些代碼,現在我們就要添加代碼了。
在這個操作方法中,我們准備做一個很簡單的任務:改變第一個視圖控制器的背景顏色,然後用UIView的動畫把它恢復到正常狀態。雖然這個操作在真正的app裡面毫無意義,但這是個好的例子,可以讓我們看到解除轉場運行的時候是怎麼操作的。
到ViewController.swift文件,在IBAction方法裡,添加如下內容:
@IBAction?func?returnFromSegueActions(sender:?UIStoryboardSegue){ ????if?sender.identifier?==?"idFirstSegueUnwind"?{ ????????let?originalColor?=?self.view.backgroundColor ????????self.view.backgroundColor?=?UIColor.redColor() ????????UIView.animateWithDuration(1.0,?animations:?{?()?->?Void?in ????????????self.view.backgroundColor?=?originalColor ????????}) ????} }
首先檢查這個解除轉場是不是我們感興趣的那個,然後把背景顏色保存在本地,改成紅色,然後使用動畫部分的塊,設回到原來的值。
以上清楚地展示了當一個自定義解除轉場被執行的時候你想執行動作的時候你應該怎麼操作。稍後我們繼續在上面的方法裡添加一些代碼。
在實現自定義轉場(和解除轉場)的時候,要注意兩點:一,注意你設置的轉換效果,以使你的app提供良好的的用戶體驗。二,執行一般的轉場或者解除轉場的時候,視圖控制器之間的數據交換。
前面部分我們已經涉及到了第一種情況,我們指定了兩個視圖來回切換的動畫轉換。現在我們要看看是怎麼傳遞數據的。很可能你已經在UIKit預定義的轉場裡用到了這個流程。
在我們開發的演示app中,我們不會在一個視圖控制器之間傳遞重要的數據。我們將簡單的發送一個字符串值給對方,會在我們早先在ViewController和SecondViewController場景裡添加的第二個標簽上顯示。
首先,打開SecondViewController.swift文件。添加以下的在IBOutlet屬性之後的屬性聲明:
var?message:?NSString!
接下來,我們要用這個屬性,從ViewController視圖到轉場執行到的視圖控制器,傳遞一個字符串值。
打開ViewController.swift文件,在類裡,添加以下方法的實現:
override?func?prepareForSegue(segue:?UIStoryboardSegue,?sender:?AnyObject?)?{ ????if?segue.identifier?==?"idFirstSegue"?{ ????????let?secondViewController?=?segue.destinationViewController?as?SecondViewController ????????secondViewController.message?=?"Hello?from?the?1st?View?Controller" ????} }
上面方法是UIViewController類的一部分。基於我們要執行的轉場的identifier,我們訪問目標視圖控制器,也就是本例中的SecondViewController。然後,給SecondViewController類裡的message屬性設置字符串消息。
以上都是你在使用轉場的時候需要傳遞數據到另一個視圖控制器所需要的。 是由很簡單的方法構成的。
現在再次打開SecondViewController.swift文件。是時候處理message屬性,和展示上面接收到的字符串。在viewDidLoad方法裡,添加如下代碼:
override?func?viewDidLoad()?{ ????... ????lblMessage.text?=?message }
最後,從第二個視圖控制到第一個發送一個字符串值。讓我們再次實現上面的方法:
override?func?prepareForSegue(segue:?UIStoryboardSegue,?sender:?AnyObject?)?{ ????if?segue.identifier?==?"idFirstSegueUnwind"?{ ????????let?firstViewController?=?segue.destinationViewController?as?ViewController ????????firstViewController.lblMessage.text?=?"You?just?came?back?from?the?2nd?VC" ????} }
上面的實現中,注意我們是直接訪問ViewController類裡的消息label的。這是展示視圖控制器子視圖數據的另一個方法。請注意,這兩種情況下有必要通過轉場對象訪問目標視圖控制器。
這幾乎就是在視圖控制器之間傳遞數據的所有步驟了。我們一會創建的第二個自定義轉場,你會看到最後一種方法,但這裡介紹的是你主要用到的。
如果你需要的話可以再運行app。這次,兩個視圖控制器裡地第二個label顯示了我們傳遞的字符串值。注意,我們在SecondViewController類裡的工作結束了,現在開始我們將關注工程裡存在的第三個視圖控制器。
之前部分我們討論到的所有概念,都是你在創建和管理自定義轉場裡需要的。然而,我認為再創建一個可以讓一切更容易消化理解,我們也可以看到一些不同的東西。所有,讓我們開始吧。
第一步,要在Interface Builder裡創建自定義轉場,所以在工程導航器裡的Main.storyboard文件裡打開它。
界面再次出現在屏幕上,打開文檔大綱板(如果是閉合的話)。點擊ViewController場景裡的ViewController對象,通過按住Ctrl和鼠標,拖動到ThirdViewController場景裡的ThirdViewController對象。將會出現一個藍色的連接線:
在彈出的窗口裡,選擇custom轉場。做完之後,確保我們剛創建的轉場在你的畫布中看起來是這樣的:
點擊上面的線,在工具板裡打開屬性檢查器。這裡你要指定兩個東西:轉場標示符和它的類。對標示符,在Identifier字段裡設置idSecondSegue值。在Segue Class字段裡,鍵入SecondCustomSegue的值,讓app知道在執行新的自定義轉場的時候要用到的類。
第二個自定義轉場已經創建好了,接下來實現我們想看到的動畫轉換。
正如我在引言裡所說,子類化UIStoryboardSegue類,重載perform方法,你可以自由地實現你想要的任何轉換。你可以有簡單地動畫轉換,也可以是復雜的,或者完全沒有動畫(但是為什麼要創建一個自定義轉場不應用任何自定義動畫效果呢?)。
這個轉場我們將指定另一種轉換到第三個視圖控制器的動畫。這次,我們將實現縮小和放大的效果。具體來說,ViewController視圖控制器的視圖通過按比例縮小視圖來消失,這會導致縮小的效果。另一方面,第三個視圖控制器的視圖將初始為極小狀態,一旦動畫執行,它將放大到狀態。這會導致放大的效果。教程的概述部分,你可以看到程序運行時的效果。
現在讓我們開始寫代碼。打開SecondCustomSegue.swift文件,和我們之前兩次做的一樣的,通過賦給局部變量創建視圖控制器的視圖的引用。當然,我們的實現將發生在perform方法裡:
override?func?perform()?{ ????var?firstVCView?=?sourceViewController.view?as?UIView! ????var?thirdVCView?=?destinationViewController.view?as?UIView! }
注意,你可以跳過此步,直接用源和目標視圖控制器屬性裡的視圖。然而,我認為那樣的話會更清晰,盡管會需要更多幾行的代碼。
現在,我們把第三個視圖控制器的視圖加到窗口上。這是你應該做的事,否則就沒有轉換過渡到得第二個視圖了。
override?func?perform()?{ ????... ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(thirdVCView,?belowSubview:?firstVCView) }
現在,我們可以指定第三個視圖控制器的視圖的初始狀態。我們不動它的結構(frame),我們修改transform屬性,代碼如下:
override?func?perform()?{ ????... ????thirdVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001) }
正如你看到的,我們按比例縮小它的寬和高。我們不設置為零,因為這樣就完全沒效果了。一個很小的值也是有利於我們的目的的。
下一步就是執行動畫。本例中,我們應用兩個連續的動畫,第一個是縮小源視圖,然後是放大目標視圖。
override?func?perform()?{ ????... ????UIView.animateWithDuration(0.5,?animations:?{?()?->?Void?in???????? ????????firstVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001)???????? ????????})?{?(Finished)?->?Void?in ????????????UIView.animateWithDuration(0.5,?animations:?{?()?->?Void?in ????????????????thirdVCView.transform?=?CGAffineTransformIdentity ????????????????},?completion:?{?(Finished)?->?Void?in ????????????????????firstVCView.transform?=?CGAffineTransformIdentity ????????????????????????????????????????self.sourceViewController.presentViewController(self.destinationViewController?as?UIViewController,?animated:?false,?completion:?nil) ????????????}) ????} }
動畫發生的時候,第一個視圖變小了,看起來像縮小的效果。在completion handler當中,我們將對目標視圖控制器反轉上面的動畫。最後,動畫結束後,我們把第一個視圖恢復到正常狀態,然後展示第三個視圖控制器。
這是整個方法:
override?func?perform()?{ ????var?firstVCView?=?sourceViewController.view?as?UIView! ????var?thirdVCView?=?destinationViewController.view?as?UIView! ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(thirdVCView,?belowSubview:?firstVCView) ????thirdVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001) ????UIView.animateWithDuration(0.5,?animations:?{?()?->?Void?in ????????firstVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001) ????????})?{?(Finished)?->?Void?in ????????????UIView.animateWithDuration(0.5,?animations:?{?()?->?Void?in ????????????????thirdVCView.transform?=?CGAffineTransformIdentity ????????????????},?completion:?{?(Finished)?->?Void?in ????????????????????firstVCView.transform?=?CGAffineTransformIdentity ????????????????????????????????????????self.sourceViewController.presentViewController(self.destinationViewController?as?UIViewController,?animated:?false,?completion:?nil) ????????????}) ????} }
自定義轉換准備好了。但是我們還沒結束,因為我們必須執行轉場。如果你記得,在ViewController場景裡有個叫“Tap Me!”的button。這是我們用來啟動轉場的。
首先,打開ViewController.swift文件,添加以下的IBAction方法:
@IBAction?func?showThirdViewController(sender:?AnyObject)?{ ????self.performSegueWithIdentifier("idSecondSegue",?sender:?self) }
如你預期的,我們只是簡單地執行轉場。注意,轉場的標示符的值必須匹配我們在Interface Builder裡設置的值。
最後,我們必須把上面的方法連到按鈕上。打開Main.storyboard.swift文件,在文檔大綱裡的ViewController場景,點在按鈕對象上,然後按住Ctrl和鼠標,拖到ViewController對象上:
出現了一個有很多選項的彈出窗口。裡面有個Sent Events,這個下面展示了IBAction方法。選擇它,聯系就成功完成了。
如果你想限制你可以測試這個app。點在最初的視圖控制器的按鈕上,觀察第三個視圖控制器是怎麼呈現的。
在演示app裡我們實現的第一個解除轉場,我說過你應該做的第一件事是IBAction方法的定義,這個方法在轉場執行的時候被調用。這裡,我們實現這樣一個方法,叫returnFromSegueActions(sender:),一旦處理好後我們也將在新的解除轉場中用到它。
上面意味著我們可以去做下一個步驟,在Interface Builder中創建解除轉場。打開Main.storyboard文件,在ThirdViewController場景中,選擇ThirdViewController對象,按住Ctrl拖動到Exit對象上。
在彈出框裡點擊操作方法的名字,解除轉場就被創建了。
接下來,選擇那個轉場,打開屬性監視器,在identifier字段裡,設置idSecondSegueUnwind值。
目前解除轉場已經准備好了,我們可以寫我們想要的轉換行為的代碼。這次,我們不僅僅執行完全相反的動畫到正常的轉場。我們將創建一個有點復雜的動畫,第三個視圖控制器的視圖將被縮小,同時移向屏幕頂端,第一個視圖控制器的視圖將被放大,從屏幕底部移動到頂端,所以占據了整個可用區域。這個視圖最初准備離開屏幕的可視區域。
打開SecondCustomSegueUnwind.swift文件,我們將最後一次實現perform方法,讓我們從簡單的開始:
override?func?perform()?{ ????var?firstVCView?=?destinationViewController.view?as?UIView! ????var?thirdVCView?=?sourceViewController.view?as?UIView! ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height }
和平常一樣,我們讓兩個視圖控制器的視圖保存為局部變量,同時把屏幕的寬和高存儲在一個變量裡。接下來,指定firstVCView視圖的初始狀態。就像我說的,首先定義離屏的位置,這個將按比例縮小:
override?func?perform()?{ ????... ????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?screenHeight) ????firstVCView.transform?=?CGAffineTransformScale(firstVCView.transform,?0.001,?0.001) ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(firstVCView,?aboveSubview:?thirdVCView) }
除了設置初始狀態,我們也把視圖添加到app的窗口上。
最後,讓我們執行動畫。記得要消失的視圖按比例縮小,要出現的視圖按比例放大,兩個視圖都往頂部移動。
override?func?perform()?{ ????... ????UIView.animateWithDuration(0.5,?animations:?{?()?->?Void?in ????????thirdVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001) ????????thirdVCView.frame?=?CGRectOffset(thirdVCView.frame,?0.0,?-screenHeight) ????????firstVCView.transform?=?CGAffineTransformIdentity ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?-screenHeight) ????????})?{?(Finished)?->?Void?in ????????????self.sourceViewController.dismissViewControllerAnimated(false,?completion:?nil) ????} }
這就是自定義解除轉場的實現。接下來是小結性的代碼:
override?func?perform()?{ ????var?firstVCView?=?destinationViewController.view?as?UIView! ????var?thirdVCView?=?sourceViewController.view?as?UIView! ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height ????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?screenHeight) ????firstVCView.transform?=?CGAffineTransformScale(firstVCView.transform,?0.001,?0.001) ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(firstVCView,?aboveSubview:?thirdVCView) ????UIView.animateWithDuration(0.5,?animations:?{?()?->?Void?in ????????thirdVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001) ????????thirdVCView.frame?=?CGRectOffset(thirdVCView.frame,?0.0,?-screenHeight) ????????firstVCView.transform?=?CGAffineTransformIdentity ????????firstVCView.frame?=?CGRectOffset(firstVCView.frame,?0.0,?-screenHeight) ????????})?{?(Finished)?->?Void?in ????????????self.sourceViewController.dismissViewControllerAnimated(false,?completion:?nil) ????} }
再次回到ViewController.swift文件,讓我們看下segueForUnwindingToViewController(toViewController:fromViewController:identifier:) 方法,我們已經討論過它了,現在我們必須檢查新的解除轉場,所以我們初始化,然後返回一個子類的轉場對象。根據我們之前說的,我們將再創建一個用例,檢查轉場的標示符是否和解除轉場的匹配,然後再繼續。通過添加如下代碼修改方法:
override?func?segueForUnwindingToViewController(toViewController:?UIViewController,?fromViewController:?UIViewController,?identifier:?String?)?->?UIStoryboardSegue?{ ????if?let?id?=?identifier{ ????????... ????????else?if?id?==?"idSecondSegueUnwind"?{ ????????????let?unwindSegue?=?SecondCustomSegueUnwind(identifier:?id, ????????????????source:?fromViewController, ????????????????destination:?toViewController, ????????????????performHandler:?{?()?->?Void?in ????????????}) ????????????return?unwindSegue ????????}???????? ????} ????... }
這沒有什麼新的或者難的地方。現在新的解除轉場可以完全執行了。然而,我們必須先啟動,通過在第三個視圖控制器裡添加新的向上滑動的手勢來管理它。所以,到ThirdViewController.swift文件,在viewDidLoad文件裡添加如下代碼:
override?func?viewDidLoad()?{ ????... ????var?swipeGestureRecognizer:?UISwipeGestureRecognizer?=?UISwipeGestureRecognizer(target:?self,?action:?"showFirstViewController") ????swipeGestureRecognizer.direction?=?UISwipeGestureRecognizerDirection.Up ????self.view.addGestureRecognizer(swipeGestureRecognizer) }
我們只剩下去實現showFirstViewController方法,和其它類似方法一樣簡單:
func?showFirstViewController()?{ ????self.performSegueWithIdentifier("idSecondSegueUnwind",?sender:?self) }
現在,運行這個差不多完全實現的應用之前,為什麼不在第三個視圖控制器消失的時候顯示一條消息呢?
當然可以。不過,和之前的方法不同,我們在IBAction這個方法returnFromSegueActions(sender:)裡傳遞數據。我們已經給它添加了一些簡單的代碼,我們也說了這個方法是隨著解除轉場用來執行各種動作的。
打開ViewController.swift文件,找到上述方法,這裡,添加以下的else代碼:
@IBAction?func?returnFromSegueActions(sender:?UIStoryboardSegue){ ????... ????else{ ????????self.lblMessage.text?=?"Welcome?back!" ????} }
上面添加的消息將每次顯示到第一個視圖控制器上,第二個解除轉場被執行。
現在我們的演示app完成了。你可以去測試了,當然,如果你想更熟悉自定義轉場你可以隨意修改任一部分。下面再次展示了app運行的動畫。
如果你快速回顧下我們在教程所做的,你肯定會得出這樣的結論:使用自定義轉場並不難。在視圖控制器間應用自定義動畫轉換,可以有很好的用戶體驗,也使得你的app和其他的與眾不同。如果自定義轉場對你來說是新的東西,去學習使用他們。我強烈建議你這麼做。我們實現的演示app可以指導你以正確的方式實現自定義轉場和解除轉場。我們創建和應用的動畫並不復雜,但足夠你理解所有的知識點。發揮你的想象力,創造更炫的效果。希望你能喜歡這篇文章,也希望它能派上用場。一如既往的,我們等待你的想法。
供您參考,您可以在這裡下載項目。
(本文為CocoaChina組織翻譯,本譯文權利歸譯者所有,未經允許禁止轉載。)