原文:http://www.cocoachina.com/ios/20150126/11011.html
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里添加如下属性:
1 | @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文件,添加以下方法定义:
1 2 3 | override?func?perform()?{ } |
这个方法已经在UIStoryboardSegue的父类里定义了,这里我们重载这个方法,以便我们添加想要的自定义逻辑。所以,第一步是使我们的工作更简单,它指定视图的源和目标视图控制器的两个局部变量,如下所示:
1 2 3 4 5 6 | 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! } |
另外,我们需要屏幕的宽和高,然后放在两个变量里:
1 2 3 4 5 6 7 8 | override?func?perform()?{ ????... ???? //?Get?the?screen?width?and?height. ????let?screenWidth?=?UIScreen.mainScreen().bounds.size.width ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height } |
好了,现在指定应该出现的视图的初始位置。考虑到我们想要视图从底部移动到上面,我们把视图放在屏幕可视区域的外面,当前视图的正下方。这很简单,我们简单地设置下视图的frame:
1 2 3 4 5 6 7 | override?func?perform()?{ ????... ???? //?Specify?the?initial?position?of?the?destination?view. ????secondVCView.frame?=?CGRectMake(0.0,?screenHeight,?screenWidth,?screenHeight) } |
这时,第二个视图控制器的视图还不是窗口的子视图。所以,在我们实现真正的动画之前,很明显,我们必须把它加到窗口上。这用窗口对象的方法insertSubview(view:aboveSubview:)来实现。正如你接下来看到的,我们先访问窗口对象,然后添加目标视图:
1 2 3 4 5 6 7 8 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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 ????} } |
上面两行代码将执行预期的效果。然而,我们还没完全结束,因为我们还没显示出新的视图控制器(目标视图控制器)。我们将在第二个闭包里实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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) ????} } |
动画部分到这里就结束了。总结一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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方法里创建一个新的手势对象,然后把它加到视图里:
1 2 3 4 5 6 7 | override?func?viewDidLoad()?{ ????... ???? var ?swipeGestureRecognizer:?UISwipeGestureRecognizer?=?UISwipeGestureRecognizer(target:?self,?action:? "showSecondViewController" ) ????swipeGestureRecognizer.direction?=?UISwipeGestureRecognizerDirection.Up ????self.view.addGestureRecognizer(swipeGestureRecognizer) } |
当手势被执行的时候,showSecondViewController方法应该是被调用的,但还没实现。因此,让我们添加它的定义,这里我们将添加简单的一行代码来执行自定义转场:
1 2 3 | func?showSecondViewController()?{ ????self.performSegueWithIdentifier( "idFirstSegue" ,?sender:?self) } |
可以注意到,我们是通过标示符来访问转场的。当我们滑动到视图,以上的调用使我们在FirstCustomSegue类里的自定义实现得以执行。
你现在可以调试app了。你可以在模拟器上运行,也可以在真机上运行。不过,即使你可以向上滑动来转换视图,你不能返回到第一个视图控制器。这是正常的,因为我们还没有定义,我们接下来将实现这个方法。
解除转场(The Unwind Segue)
创建自定义转场的时候,需要同时实现导航里的两个方法:从源视图控制器到目标视图控制器,然后返回。几乎不可能只建立一个方法。现在我们已经实现了转场,所以它将按我们想要的动画方式显示第二个视图控制器,我们必须让app能够做相反的事情。执行返回导航的转场,叫解除转场(unwind segue)。
简而言之,解除转场和正常的一样,但是它的配置有点复杂。某些步骤还是要完成的,但没有超出标准编程技术之外的东西。注意两点:
解除转场(unwind segue)通常和正常自定义转场(segue)一起出现。
要解除转场起作用,我们必须重写perform方法,并应用自定义逻辑和我们想要的或者应用规定的动画。另外,导航返回源视图控制器的过渡效果不需要和对应的正常转场相同。
解除转场的实现分四个步骤:
IBAction方法的创建,该方法在解除转场被执行的时候会选择地执行一些代码。这个方法可以有你想要的任何名字,而且不强制包含其它东西。它需要定义,但可以留空,解除转场的定义需要依赖这个方法。
解除转场的创建,设置的配置。这和之前我们在Interface Builder里的转场不太一样,等下我们将看看这个是怎么实现的。
通过重写UIStoryboardSegue子类里的perform方法,来实现一般的逻辑。
UIViewController类提供了特定方法的定义,所以系统知道解除转场即将执行。
现在你可以会有些迷糊,接下来,我们会解释所有的东西。
先从上面提到的第一个步骤里的IBAction方法开始。这个方法通常在我们想要导航的初始视图控制器里实现。如我所说,方法里可以是空的,但是你得记住如果你不定义至少一个这样的方法,你将无法继续。当解除转场存在在app中,没必要使用那么多的方法。通常情况下,简单地定义一个这样的动作方法,检测对应的解除转场对应然后调用,就足够了。
现在行动,打开ViewController.swift文件,添加如下代码:
1 2 3 | @IBAction?func?returnFromSegueActions(sender:?UIStoryboardSegue){ } |
我们将稍后添加些示例代码。注意这个方法的参数是一个UIStoryboardSegue对象,这个是需要记住的细节。
现在让我们去Interface Builder里,打开Main.storyboard文件。是时候创建解除转场了。我们开始检查SecondViewController场景里包含的对象,甚至查看场景本身,或者文档大纲。如果你仔细观察,你会发现一个对象,叫Exit:
如果你还没创建过自定义转场,很可能你不会用到它,可能会想知道这个干什么的。这个就是你需要创建的解除转场。
点击场景里SecondViewController对象,按Ctrl拖到Exit对象:
下面的弹出框会显示我们早前定义的IBAction方法:
确保选中它。解除转场将被创建,你可以在文档大纲里查看到。现在新的联线被创建了,但转场在这里:
现在点击我们刚创建的解除转场,打开属性检查器。设置“idFirstSegueUnwind”作为转场的标示符:
解除转场的第二部分目前已经完成了。接下来我们要开始写代码了,并确定解除转场的执行方法。
首先,打开FirstCustomSegueUnWind.swift文件,再次重写perform方法,并保持对视图的局部变量的引用。同时,我们也要在一个变量中保存屏幕高度:
1 2 3 4 5 6 7 | 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。继续,这里我们不指定任何视图的初始状态,我们只把第一个视图控制的视图(我们想要返回的那个)加到窗口的子视图上:
1 2 3 4 5 6 7 | override?func?perform()?{ ????... ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(firstVCView,?aboveSubview:?secondVCView) } |
最后,我们必须激活两个视图的运动。这次,他们都往底部移动:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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值,我们设法把他们在预定义动画的时间内放到新的位置上。当转换结束,我们只要让第二个视图控制器直接消失,不带任何动画,然后就结束了。下面一起给你这个方法的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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文件,定义一下方法:
1 2 3 | override?func?segueForUnwindingToViewController(toViewController:?UIViewController,?fromViewController:?UIViewController,?identifier:?String?)?->?UIStoryboardSegue?{ } |
这是UIViewController类提供的方法。在解除转场执行的时候会自动调用这个方法,其实现是手动添加的。有三个参数:
fromViewController: 当前显示的视图控制器,是我们想让它消失的。
toViewController: 目标视图控制器,或者换句话说是我们想显示的控制器。
identifier: 将被执行的转场的标示符。
注意,这个方法返回的是一个转场对象。我们要做的很简单:我们会用identifier的值来决定要执行的那个转场,然后初始化一个解除转场自定义类的对象,这个类对象是我们最后要返回的。同时,调用一个super的方法,在这里重载下,让我们看看代码:
1 2 3 4 5 6 7 8 9 10 11 12 | 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方法里。添加如下代码:
1 2 3 4 5 6 7 8 | override?func?viewDidLoad()?{ ????... ???? var ?swipeGestureRecognizer:?UISwipeGestureRecognizer?=?UISwipeGestureRecognizer(target:?self,?action:? "showFirstViewController" ) ????swipeGestureRecognizer.direction?=?UISwipeGestureRecognizerDirection.Down ????self.view.addGestureRecognizer(swipeGestureRecognizer) } |
上面定义的手势是向下滑动的手势,刚好与之前的相反。
现在定义showFirstViewController方法:
1 2 3 | func?showFirstViewController()?{ ????self.performSegueWithIdentifier( "idFirstSegueUnwind" ,?sender:?self) } |
现在一切都准备好了,你可以再次调试app。这次,你可以在视图控制器之间来回导航。
执行操作
前面的部分,我们声明了一个空方法:returnFromSegueActions(sender:), 我们说这个方法是必须的,这样我们才能创建解除转场。同时,我也说过后面会添加一些代码,现在我们就要添加代码了。
在这个操作方法中,我们准备做一个很简单的任务:改变第一个视图控制器的背景颜色,然后用UIView的动画把它恢复到正常状态。虽然这个操作在真正的app里面毫无意义,但这是个好的例子,可以让我们看到解除转场运行的时候是怎么操作的。
到ViewController.swift文件,在IBAction方法里,添加如下内容:
1 2 3 4 5 6 7 8 9 10 | @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属性之后的属性声明:
1 | var ?message:?NSString! |
接下来,我们要用这个属性,从ViewController视图到转场执行到的视图控制器,传递一个字符串值。
打开ViewController.swift文件,在类里,添加以下方法的实现:
1 2 3 4 5 6 | 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方法里,添加如下代码:
1 2 3 4 5 | override?func?viewDidLoad()?{ ????... ????lblMessage.text?=?message } |
最后,从第二个视图控制到第一个发送一个字符串值。让我们再次实现上面的方法:
1 2 3 4 5 6 | 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方法里:
1 2 3 4 5 | override?func?perform()?{ ???? var ?firstVCView?=?sourceViewController.view?as?UIView! ???? var ?thirdVCView?=?destinationViewController.view?as?UIView! } |
注意,你可以跳过此步,直接用源和目标视图控制器属性里的视图。然而,我认为那样的话会更清晰,尽管会需要更多几行的代码。
现在,我们把第三个视图控制器的视图加到窗口上。这是你应该做的事,否则就没有转换过渡到得第二个视图了。
1 2 3 4 5 6 7 | override?func?perform()?{ ????... ????let?window?=?UIApplication.sharedApplication().keyWindow ????window?.insertSubview(thirdVCView,?belowSubview:?firstVCView) } |
现在,我们可以指定第三个视图控制器的视图的初始状态。我们不动它的结构(frame),我们修改transform属性,代码如下:
1 2 3 4 5 6 | override?func?perform()?{ ????... ????thirdVCView.transform?=?CGAffineTransformScale(thirdVCView.transform,?0.001,?0.001) } |
正如你看到的,我们按比例缩小它的宽和高。我们不设置为零,因为这样就完全没效果了。一个很小的值也是有利于我们的目的的。
下一步就是执行动画。本例中,我们应用两个连续的动画,第一个是缩小源视图,然后是放大目标视图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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当中,我们将对目标视图控制器反转上面的动画。最后,动画结束后,我们把第一个视图恢复到正常状态,然后展示第三个视图控制器。
这是整个方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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方法:
1 2 3 | @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方法,让我们从简单的开始:
1 2 3 4 5 6 7 | override?func?perform()?{ ???? var ?firstVCView?=?destinationViewController.view?as?UIView! ???? var ?thirdVCView?=?sourceViewController.view?as?UIView! ????let?screenHeight?=?UIScreen.mainScreen().bounds.size.height } |
和平常一样,我们让两个视图控制器的视图保存为局部变量,同时把屏幕的宽和高存储在一个变量里。接下来,指定firstVCView视图的初始状态。就像我说的,首先定义离屏的位置,这个将按比例缩小:
1 2 3 4 5 6 7 8 9 10 11 | 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的窗口上。
最后,让我们执行动画。记得要消失的视图按比例缩小,要出现的视图按比例放大,两个视图都往顶部移动。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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) ????} } |
这就是自定义解除转场的实现。接下来是小结性的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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:) 方法,我们已经讨论过它了,现在我们必须检查新的解除转场,所以我们初始化,然后返回一个子类的转场对象。根据我们之前说的,我们将再创建一个用例,检查转场的标示符是否和解除转场的匹配,然后再继续。通过添加如下代码修改方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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文件里添加如下代码:
1 2 3 4 5 6 7 | override?func?viewDidLoad()?{ ????... ???? var ?swipeGestureRecognizer:?UISwipeGestureRecognizer?=?UISwipeGestureRecognizer(target:?self,?action:? "showFirstViewController" ) ????swipeGestureRecognizer.direction?=?UISwipeGestureRecognizerDirection.Up ????self.view.addGestureRecognizer(swipeGestureRecognizer) } |
我们只剩下去实现showFirstViewController方法,和其它类似方法一样简单:
1 2 3 | func?showFirstViewController()?{ ????self.performSegueWithIdentifier( "idSecondSegueUnwind" ,?sender:?self) } |
现在,运行这个差不多完全实现的应用之前,为什么不在第三个视图控制器消失的时候显示一条消息呢?
当然可以。不过,和之前的方法不同,我们在IBAction这个方法returnFromSegueActions(sender:)里传递数据。我们已经给它添加了一些简单的代码,我们也说了这个方法是随着解除转场用来执行各种动作的。
打开ViewController.swift文件,找到上述方法,这里,添加以下的else代码:
1 2 3 4 5 6 7 | @IBAction?func?returnFromSegueActions(sender:?UIStoryboardSegue){ ????... ???? else { ????????self.lblMessage.text?=? "Welcome?back!" ????} } |
上面添加的消息将每次显示到第一个视图控制器上,第二个解除转场被执行。
运行应用程序
现在我们的演示app完成了。你可以去测试了,当然,如果你想更熟悉自定义转场你可以随意修改任一部分。下面再次展示了app运行的动画。
总结
如果你快速回顾下我们在教程所做的,你肯定会得出这样的结论:使用自定义转场并不难。在视图控制器间应用自定义动画转换,可以有很好的用户体验,也使得你的app和其他的与众不同。如果自定义转场对你来说是新的东西,去学习使用他们。我强烈建议你这么做。我们实现的演示app可以指导你以正确的方式实现自定义转场和解除转场。我们创建和应用的动画并不复杂,但足够你理解所有的知识点。发挥你的想象力,创造更炫的效果。希望你能喜欢这篇文章,也希望它能派上用场。一如既往的,我们等待你的想法。
供您参考,您可以在这里下载项目。
(本文为CocoaChina组织翻译,本译文权利归译者所有,未经允许禁止转载。)