iOS 中使用ViewController控制转场的各种方法

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);
StoryboardSegue.gif

(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)
StoryboardCode.gif

(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之后将背景色改成紫色:

MySegue.png

方法(1),直接在Storyboard里选择Custom Segue:


mySegue.gif

你也可以直接初始化一个MySegue,然后手工调用perform:


CustomSegueCode.png

最后,使用dismiss方法退回presenting VC:

screenshots3.gif
screenShot3.gif

关于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 值决定的动画:

TransitionStyle.png

你也可以使用(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:


showDetailViewController.png
showDetailViewController.gif

(5)presentViewController:animated:completion:

这是除(4)之外另一个常用的简单方法,相比之下,它的优势是可以控制是否显示动画开关和completion block,能够让你实现更多的自定义功能,但是它总是modally显示新的VC。一般情况下,horizontally compact(iphone) 环境下,推荐使用这个方法。

demo示例:用presentViewController来展示A,并且在展示结束时使得A的背景色改为紫色:

屏幕快照 2016-03-28 下午3.44.27.png
screenShot5.gif

(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:


addChildViewController.png

在View A上增加返回View B的按钮,调用removeFromParentViewController和removeFromSuperView:


removeFromParentViewController.png

Demo演示:

screenShot7.gif

(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。


屏幕快照 2016-03-28 下午4.45.38.png
transitionFromAToC.gif

以上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显示自己的类名:

nameLabel.png

接着我们用方法(1)通过Storyboard Segue在View A上增加展示出View C的按钮,然后我再来看下A和C上在不同的场景下显示出来的presenting VC是什么:

  • 当从View B展示出View A,然后在View A上显示到View C时:
screenShot9.gif

可以看到,C上显示的presenting VC不是A,而是B,这是因为调用presentViewController的A此时并不是全屏的VC,它不能modally handle presentation,必须通过它的父级B才能完成;

- 另一种情况,从main VC全屏present到A,然后再通过Add展示

screenShot9.gif

这时就可以看到在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进行不同页面之间跳转显示的大部分方法。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容