iOS中,使用ViewController進行頁面跳轉的方法有很多,之前總是想到哪用到哪,最近在review項目的code時候,抽空整理了一下,給自己理順思路。
由于iOS中MVC的概念,view很多時候和ViewController是關聯在一起的,這就決定了View的展示可以通過直接操作View和間接操作ViewController兩種情況。第一種的核心其實就是addSubView,而第二種的核心,其實是ViewController之間的層級關系。本文主要針對相對復雜的第二種情況。
一上來就說結果。目前我整理出來的,通過操作ViewController進行不同的View跳轉的方法一共有以下2大類9種:
1. Sibling Views Present
- Segue
- 直接使用Storyboard - (1)
- Storyboard和代碼混合 - (2)
- 純手工Segue - (3)
- showViewController:sender: / showDetailViewController:sender: - (4)
- presentViewController:animated:completion: - (5)
2. Container Views Present
- 系統自定義UINavigationController, UITabBarController, UISplitController - (6)
- addChildViewController: / removeFromParentViewController - (7)
- transitionFromViewController:toViewController:duration:options:animations:completion: - (8)
- 完全自定義transition - (9)
這里用一個demo來復習下上述9種種除系統自定義Controller跳轉和完全自定義transition兩種之外的其他7個場景。
首先看下demo的結構:一共有Main, A, B, C 4個View。其中Main是主入口。
1)Main到A, Main到B演示第一大類的5種方法;
2)B到A演示第(7)種
3)B到A和C演示第(8)種
在描述下述幾個方法之前,首先需要理解一些基本概念:
展示一個VC會在原VC和新VC之間建立一個關聯,其中原始的那個VC叫做presenting view controller,被展示出來的那個新的VC叫做presented view controller。這種關聯形成了VC之間的層級關系并且會一直持續到presented VC被dismiss掉。
(1)直接使用Storyboard
- 在presenting VC上ctrl-click跳轉的源物件(此源物件必須為view 或者其他具有明確定義action的object,比如control,bar button item, gesture recognizer等等),拖拽到presented VC上;
- 指定segue type (注意這里有Adaptive Type和Nonadaptive Type之分,后者為兼容iOS 7所使用);
- 在attributes inspector中指定Segue id;
- 在shouldPerformSegueWithIdentifier:sender:中自定義跳轉的先決條件;
- 在prepareForSegue:sender:中處理VC之間的數據傳遞多重Segues跳轉;
- 在presented VC中ctrl-click拖拽到VC上部的Exit按鈕,使用Unwind Segue在Storyboard中自動設置dismiss掉已經被presented出來的 VC(注意必須在拖拽前在presenting VC中定義unwind方法: (IBAction)myUnwindAction:(UIStoryboardSegue*)unwindSegue);
(2)Storyboard和Code混合
上述方法(1)必須從確定的presenting VC的某個物件上拖拽Segue到指定的presented VC上。如果不想指定特定的源物件,或者你的Segue的發生地點和時間不確定等等,你可以直接使用performSegueWithIdentifier:sender: 觸發Segue:
- 在Storyboard中presenting VC附近的空白處雙擊;
- 待顯示頁面縮小后,從presenting VC處ctrl-click拖拽至presented VC上;
- 在系統提示出的Segue上指定id;
- 在presenting VC中需要跳轉此Segue的地方,調用performSegueWithIdentifier:sender: 其中的Identifier就是你在上一步中設定的id;
- 其他同(1)
(3)純Code手工Segue
如果Storyboard預定義的幾種segue不能滿足你的需求,你可以用code定制化一個Segue:
- Subclass 系統提供的UIStoryboardSegue類,實現以下方法:
- 重寫initWithIdentifier:source:destination:方法;
- 在perform方法中配置轉場動畫;
- 調用performSegueWithIdentifier:sender: 觸發剛才創建好的自定義Segue;
- 需要退出presented VC時,調用dismissViewControllerAnimated:completion:方法就可以。
Demo:
首先,我們創建一個自定義Segue:MySegue。這里簡單起見,perform方法只是調用后文提到的presentViewController。新的VC被present之后將背景色改成紫色:
方法(1),直接在Storyboard里選擇Custom Segue:
你也可以直接初始化一個MySegue,然后手工調用perform:
最后,使用dismiss方法退回presenting VC:
關于Presentation Style,Presentation Context 和 transition Style
在介紹(4)(5)之前先了解下這幾個概念。iOS設備的屏幕在方向上分為vertical, horizontal兩種,每個方向的大小上又分為 compact,regular,Any三種,一共組合起來有3*3=9種不同的適配模式(叫做size class)。如果你沒有特殊指定在特定的size class下使用哪種特殊的presentation style,presenting VC會替你選擇最優的方式并且自動給你調整layout constrains。
Presentation Style
Presentation Style是iOS提供的多種默認的展示方式,分為:
-
Full-Screen Presentation Styles:
全屏模式會阻塞住下層整個屏幕的交互。在horizontally regular屏幕下(針對iPad),根據不同的值可能會部分或全部遮擋屏幕可視部分,如下圖:
FullScreen.png
而在horizontally compact 環境下(主要針對iPhone),無論你選擇什么參數,最終都會使用UIModalPresentationFullScreen而覆蓋下層整個屏幕的內容。
注意1:使用UIModalPresentationFullScreen style時,UIKit通常會在animation結束后remove掉下層被遮擋的View 。如果不希望這樣,比如當展示一個透明的View的時候希望能顯示下層的內容,就可以使用 UIModalPresentationOverFullScreen 值。
注意2:在Full Screen下,最終的presenting VC不一定是你實際調用的presenting VC。UIKit會回溯你的VC hierachy來找到最近的一個全屏的VC來控制presentation過程,如果找不到,最終會選擇window的root VC來做presenting VC。在后文第(8)種方法之后我們的Demo將會來驗證這個事情。
- Popover Presentation Style:
對應于UIModalPresentationPopover 值。在horizontally Regular下(主要針對iPad屏幕),會出現下圖的樣子:
Popover.png
由于Popover style只會遮住一部分的屏幕區域,點擊這個區域之外的部分會自動dismiss掉Presented VC。
而在horizontally Compact下(主要針對iPhone),Popover樣式會自動適配到UIModalPresentationOverFullScreen Style。這種情況下,由于會覆蓋整個屏幕,你需要自己設計一種退出的方法,可能是加一個退出按鈕,或者是把popover遷入到另一個單獨的container VC中等等。
- Current Context Styles
對應于UIModalPresentationCurrentContext 值。這里需要提出一個概念Presentation Context。Apple并沒有單獨提出Presentation Context這樣的詞匯,而是在API中使用了這個詞。按照我自己的理解,對于iPad這樣的大屏設備,一個屏幕里會出現多個VC,比如分屏的Split VC的使用頻率就比較高。Presentation Context的概念就是當你調用下述(4)(5)的方法時,提前在這些VC中指定替換哪個VC,這個VC就作為調用presentation時的current context。Current Context Sytles 就是為這種情況準備的。先將你想指定的VC的屬性definesPresentationContext設置為YES,然后使用UIModalPresentationCurrentContext style 就會替換指定的VC。如下圖所示:
PresentationContext.png
在horizontally compact環境下,current context styles將自適應到UIModalPresentationFullScreen。
同理,你可以使用UIModalPresentationOverCurrentContext來阻止UIKit自動移除下層的View。
- Custom Presentation Style
這個是對應于第(9)種自定義轉場的高階樣式。細節可以參考(9)中的文章,在此暫時不表。
Transition Style
Transition Style決定了顯示presented VC的動畫樣式。UIKit內置了很多預定義的動畫,這些動畫就取決于你在presented VC中設置的modalTransitionStyle屬性。比如下圖所示的 UIModalTransitionStyleCoverVertical 值決定的動畫:
你也可以使用(9)中描述的方法來自定義更加復雜的顯示動畫。
(4)showViewController:sender: / showDetailViewController:sender:
這是展示一個新的VC最簡單也是最有效的方法,也是Apple推薦的方法。原因是這些方法能夠讓presented VC自由的自動選擇最佳的展示方式來展示presenting VC,你自己不用操心presenting VC和presented VC是在一個Navigation Controller或者是在一個split-view Controller里,你也不用關心具體的動畫流程。一切都交給UIKit自己去完成。使用方法:
- 創建presented VC;
- 設置presented VC的modalPresentationStyle屬性(但有可能最終無效),如果不設置將使用系統默認值;
- 設置presented VC的modalTransitionStyle屬性(但有可能最終無效),如果不設置將使用系統默認值;
- 調用 showViewController:sender: 或者 showDetailViewController:sender:
showViewControlle:sender: 和showDetailViewController:sender:之間的區別是前者默認替換的是Primary context VC,而后者是替換Secondary context VC。你可以重寫這兩個方法來自己顯示presented
VC,但是應當確保它們各自操作的context和系統默認定義的規則一致。
Demo Code:使用showDetailViewController來展示View B:
(5)presentViewController:animated:completion:
這是除(4)之外另一個常用的簡單方法,相比之下,它的優勢是可以控制是否顯示動畫開關和completion block,能夠讓你實現更多的自定義功能,但是它總是modally顯示新的VC。一般情況下,horizontally compact(iphone) 環境下,推薦使用這個方法。
demo示例:用presentViewController來展示A,并且在展示結束時使得A的背景色改為紫色:
(6)系統自定義UINavigationController, UITabBarController, UISplitController
這幾個是iOS提供的內置Container VC,這個不在此多說,可以參考Apple的開發文檔。
在早起版本的iOS里,這是唯一可以使用的Container VC,絕大多數情況下已經夠用了。但是有些情況下這些預定義Container并不夠用,這種情況下,增加一個新的頁面只能通過addSubView的方式,從而造成一個VC可能會接管大量的sub view,這不但會造成VC的臃腫不方便代碼的維護,也會造成層級結構的不清晰。所以從iOS7之后,Apple提供了可以自定義Container VC的方式,于是就有了下面的(7)(8)兩種新的方法。
(7) addChildViewController: / removeFromParentViewController
嚴格意義上來說,這其實并不是直接進行View之間切換的方法,因為此方法需要UIView的addSubView:方法的配合。但是這種方法的核心思想卻是addSubView所沒有的,那就是在Container VC中創建的父子關系。
使用addChildViewController: 方法時,需要特別注意調用次序:
- Parent VC(presenting VC)調用addChildViewController: 方法;
- 注意調整Child VC的root view的大小和位置;
- 調用Parent View 的addSubView: 方法加載Child VC的root view;
- 在完成Child VC的設置之后,必須調用Child VC的didMoveToParentViewController: 方法,這是因為只有你自己才能知道什么時候Child VC已經設置完成并被加載到容器里眾多View的層級結構恰當的位置;
對應的,當刪除Child VC時:
- Child VC首先調用 willMoveToParentViewController:nil 方法,注意參數nil,這是讓UIKit能夠知道你想移除VC之間的父子關系;
- 移除view之間必要的layout限制關系;
- 調用Child View的removeFromSuperView方法;
- 調用Child VC的removeFromParentViewController方法;
實際上,willMoveToParentViewController:和didMoveToParentViewController:應該成對出現,只是UIKit自動替你完成了部分工作,調用addChildViewController:時已經調用了前者,調用removeFromParentViewController:時已經替你調用了后者。
Demo示例:在View B上調用addChildViewController和addSubView展示出View A,并且調整A的大小為150*150:
在View A上增加返回View B的按鈕,調用removeFromParentViewController和removeFromSuperView:
Demo演示:
(8)transitionFromViewController:toViewController:duration:options:animations:completion:
這個方法其實是上述(7)在多個Child VC場景下的進階版,UIKit替你考慮到了一個最常見的場景,就是一個Container VC需要在多個Child VC之間進行切換,比如Navigation Controller需要不斷替換自己的幾個子View界面。這個時候你就可以直接調用此方法。
注意,調用這個方法有一個明確的前提:FromViewController和toViewController都必須是調用者(Presenting VC)得Child VC,如果沒有提前建立父子關系,系統運行時會crash。所以,在調用此方法前必須將fromViewController移除Parent VC,將toViewController加進Parent VC,同時在對應的時序點調用各自對應的willMoveToParentViewController:和didMoveToParentViewController:方法。
Demo示例:首先在View B上通過方法(7)添加Child VC展示出View A,然后通過transition方法替換View A到View C。
以上8種再加上單獨調用UIView的addSubView:, transitionWithView:duration:options:animations:completion:
, 和transitionFromView:toView:duration:options:completion:已經基本上能夠滿足絕大多數頁面跳轉需求了。
上文有提到,presentation的最終執行者(也就是presenting VC)并不一定就是你調用presentation的VC,這點我們在Demo中來看一下:
我們在View A, View C中添加一個Label,在顯示出View的時候,將當前的presenting VC的class名字顯示在label上,同時為了方便看清楚當前的View是誰,增加一個nameLabel顯示自己的類名:
接著我們用方法(1)通過Storyboard Segue在View A上增加展示出View C的按鈕,然后我再來看下A和C上在不同的場景下顯示出來的presenting VC是什么:
- 當從View B展示出View A,然后在View A上顯示到View C時:
可以看到,C上顯示的presenting VC不是A,而是B,這是因為調用presentViewController的A此時并不是全屏的VC,它不能modally handle presentation,必須通過它的父級B才能完成;
- 另一種情況,從main VC全屏present到A,然后再通過Add展示
這時就可以看到在A和C之間互相展示時,presenting VC各自都是對方。
這里留一個思考題:在第一種情況下,為什么從B展示A時,A的presenting VC為main VC(就是名為ViewController),而不是ViewController B ?(注意B是由Main VC 調用showDetailViewController 展示出來)
(9)完全自定義轉場
最后一種高階用法,將給你最大的自由度去定制化跳轉過程,但是相對而言也是最復雜的一種。核心是將上述的轉場過程中的控制單元全部暴露出來讓你一個個的定制化,包括:
- 轉場代理(Transition Delegate)
- 動畫控制器(Animation Controller)
- 交互控制器(Interaction Controller)
- 轉場環境(Transition Context)
- 轉場協調器(Transition Coordinator)
這種用法展開描述篇幅很大,這里有兩篇文章可以供您參考,一篇是Apple的官方文檔:
Customizing the Transition Animations
一篇是大神唐巧的文章:
iOS 視圖控制器轉場詳解
OK,希望能幫助到閱讀到此文的讀者快速掌握iOS中使用VC進行不同頁面之間跳轉顯示的大部分方法。