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),根据不同的值可能会部分或全部遮挡屏幕可视部分,如下图:
而在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 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。如下图所示:
在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进行不同页面之间跳转显示的大部分方法。