iOS中的跳转动画总结

不管任何App,对于跳转这个最基础的动画都是必须的,而iOS的转场动画不仅只是动画,还涉及层级、跳转限制、携带参数...等等一系列问题。所以捋清楚专场动画的不同和使用方式显得尤为重要,现在总结一下涉及该部分的技术参数内容如下:

Modal presentation

  1. 设定ViewController的modalTransitionStyle属性。
    这种方式也对应Storyboard中对应的segue transition的设置。这个属性是一个枚举类型,其值代表已经定制的几种转场风格。这种方式也是最简单的转场,不带有任何自定义的转场效果代码。
  2. 使用UIView的animation API实现自定义的动画。

这种方式是比较常见实现方式。除了官方的文档以外,大量的Blog文章都会详细讲解这些API的用法。UIView的animation API的使用比较直观,相对来说也是一种比较容易学习的动画实现方式。

Navigation View Controller presentation

  1. 使用UIView的animation API。

与Modal presentation相同,尽管在Storyboard中有专门为Navigation View Controller定制的Push segue(iOS8中被Show segue取代,因为后者支持iOS8中引进的Adaptive AutoLayout),但是Push segue并没有transtion属性,所以如果需要定制转场效果,可以使用UIVIew,使用方法和Modal presentation相同。

  1. 使用CATransition类。

CATransition 看起来更像为Navigation View Controller和TabBar View Controller这样的容器Controller定制的转场效果类。提供了很多内置的的动画效果。CATransition还可以结合 CoreImage的滤镜CIFilter共同实现很炫的场景转换。若想详细了解CATransition的用法,可以读一读苹果的文档。

我 们注意到,转场往往发生在流程切换的时候。所以上面的转场效果代码,往往会放在自定义的转场方法中(多见于使用Nib开发)或放在自定义的 UIStoryboardSegue类中(多见于使用Storyboard开发)。所以很多时候,我们往往会碰到原生的转场方法与定制的动画效果有一定冲 突。因为像presentViewController:animated:completion:这类方法本身就自带有内置的动画效果,自定义的动画效 果往往在这个方法之外。所以很多时候需要用一些trick来避免这些问题。因此代码的可读性往往不会很好,并且写的不好的时候还会带来效率方面的问题。

iOS7以后,苹果引进了新的Transition API。这些API的使用方式,苹果没有给出一个官方的Guide,但是在网上,已经有很多Blog和教程讲解如何使用这些API,比如这篇文章。

新 的Transition API完全改变了上面提到的动画与原生转场接口不兼容的问题。在新的API中,我们可以将动画效果代码单独封装到animator对象中,在设定好 View Controller的transitoningDelegate后,再调用原生的转场方法,就会自动使用定制的动画效果。考虑到现在大部分App已经逐 渐放弃了对iOS6的支持,所以这种方法是目前推荐的转场效果定制方法。单独封装的动画效果类在代码管理上也更加方便。

这 里需要注意的一点,iOS6中引入的Storyboard Unwind Segue往往都需要一个Container View Controller。一个很常见的问题就是新手在定制Segue的时候往往会发生自定义的Unwind Segue不起作用。这个问题一般都是由于没有正确实现Container View Controller所需的方法带来的。

Storyboard中的转场

自 iOS5引入Storyboard之后,iOS开发者在除了原有的Nib开发的基础上又有了新的方式来组织自己的UI和流程。Storyboard相对于 传统的Nib,能够更加清晰的体现业务的流程,因此很受开发者欢迎。如今,很多教程都以Storyboard开发方式来讲解。而Storyboard中的 Segue则是对转场流程的进一步封装。这个概念在Storyboard中至关重要,也是实现自定义转场的关键角色。

自定义Segue

自定义Segue的方式很简单,只要创建一个UIStoryboardSegue子类,并实现其perform方法即可。一个简单的实现如下:

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (void)perform
{
// Modal presentation segue
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
[fromController presentViewController:toController animated:YES completion:^{
// Completion code here
}];
}
或者

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (void)perform
{
// Navigation ViewController segue(Push segue, Show segue in iOS8).
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
[fromController.navigationController pushViewController:toController animated:YES];
}

自定义Unwind Segue

自 定义Unwind Segue的方式与上面几乎完全一样,只不过调用的接口由presentViewController:animated:completion:和 pushViewController:animated:换成dismissViewControllerAnimated:completion:和 popToViewController:animated:。

但是Unwind Segue与普通的Segue有一个很大的不同,就是Unwind Segue的调用通常是由一个Container View Controller完成的。在iOS SDK的UIKit框架中,Navigation View Controller和TabBar View Controller都是常用的Container View Controller。

那么为什么Unwind Segue需要一个Container View Controllerl的支持?

这 里就需要提一下Unwind Segue的设计初衷及其工作方式。之所以引入Unwind Segue,是为了应付任意跳转的情况,即从任意一个View Controller转场到特定的View Controller。在Nib的时代,这种工作往往通过delegate来完成。但是有了Unwind Segue以后,我们只要在需要跳转到的这个特定的View Controller类中实现一个签名为- (IBAction)unwindMethod:(UIStoryboardSegue *)segue这样的方法即可(其中unwindMethod可以替换为任何你喜欢的名称,但注意,当存在多个这样的方法时,名称不要相同,以免发生冲 突,造成不可预料的后果)。这样,我们就可以在任意的View Controller(除了含有这个方法本身的View Controller)通过连接Segue来实现任意View Controller跳转到当前View Controller。不用再多写一行代码,这些都可以通过Interface Builder搞定,非常方便。

Unwind Segue的工作原理大致如下:

● 当我们通过UI事件或手动调用performSegueWithIdentifier:sender:方法触发一个Unwind Segue以后,首先UIKit会发送 canPerformUnwindSegueAction:fromViewController:withSender:消息到 sourceViewController询问是否处理UnwindSegue的action,由于sourceViewController不能处理 (Unwind到自身没有意义),会返回NO

● UIKit然后会寻找sourceViewController的父Controller。如果sourceController是嵌入 Navigation View Controller的子Controller,那么父Controller就是其navigationController

● 之后UIKit会发送 viewControllerForUnwindSegueAction:fromViewController:withSender:消息给 navigationController,询问能否找到一个负责处理此action的子Controller

● 在navigationController的默认 viewControllerForUnwindSegueAction:fromViewController:withSender:实现 中,navigationController会向自己的navigation栈上的所有子Controller发送

● canPerformUnwindSegueAction:fromViewController:withSender:消息。 UIViewController类中,该方法的默认实现会查看unwinde segue action定义是否存在(即上面提到的特定签名的方法是否存在,这个方法的内部实现可以留空),若存在就返回YES。

● 如果navigationController的viewControllerForUnwindSegueAction:fromViewController:withSender:方法返回nil,则不会触发任何Unwind Segue

● 如果navgationController找到一个子类可以处理Unwind Segue的action,那么UIKit会发送 segueForUnwindingToViewController:fromViewController:identifier:消息给 navigationController,此方法将返回一个实际执行定制转场的segue实例

● 调用sourceViewController上的prepareForSegue:sender:方法

● 调用由viewControllerForUnwindSegueAction:fromViewController:withSender:方法返回的destinationViewController中的Segue action方法

● 调用Unwind Segue实例中的perform方法

从 上面的我们可以知道,Unwind Segue的正常工作必须要有一个Container View Controller作为所有流程View Controller的父Controller来管理整个流程。在上面的原理说明中,这个父Controller是Navigation View Controller。如果我们要实现一个自己的定义的Container,就必须给自定义的View Controller类实现一些上面提到过的方法:

● canPerformUnwindSegueAction:fromViewController:withSender:

● viewControllerForUnwindSegueAction:fromViewController:withSender:

● segueForUnwindingToViewController:fromViewController:identifier:

关于这些方法的说明和实现方式,下面就来详细讨论一下。

自定义Container

实现自定义的Container View Controller

我们一般会在子Controller中通过实现canPerformUnwindSegueAction:fromViewController:withSender:来决定要不要执行相应的Unwind Segue。

在 自定义的容器中,我们必须实现 viewControllerForUnwindSegueAction:fromViewController:withSender:和 segueForUnwindingToViewController:fromViewController:identifier:方法。前一个方法 用来决定那个View Controller来处理Unwind Segue action,后一个方法用来返回自定义的Unwind Segue实例。

使用Modal presentation时需要注意的情况

当 我们使用UIViewController的presentViewController:animated:completion:方法以Modal presentation的方式来跳转场景的时候,情况与在Navigation View Controller有很大不同。首先,使用这种方式跳转场景的时候,跳转到的View Controller为Source View Controller的子Controller,而在Navigation View Controller中,所有的流程Controller基本上都是Navgation View Controller的子Controller,所以二者在View Controller的层次管理上有很多不同。因此实现Modal presentation风格的Segue的时候,动画的view不能搞错,必须对View Controller中的顶层View操作。一个参考实现如下(略掉动画效果代码,仅提供转场方法调用代码):

Segue部分:

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (UIView *)findTopMostViewForViewController:(UIViewController *)viewController
{
UIView *theView = viewController.view;
UIViewController *parentViewController = viewController.parentViewController;
while (parentViewController != nil)
{
theView = parentViewController.view;
parentViewController = parentViewController.parentViewController;
}
return theView;
}
- (void)perform
{
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
// Find the views that we will be animating. If the source or destination
// view controller sits inside a container view controller, then the view
// to animate will actually be that parent controller's view.
UIView *sourceView = [self findTopMostViewForViewController:source];
UIView *destinationView = [self findTopMostViewForViewController:destination];
[source presentViewController:destination animated:NO completion:^{
// completion code here
}];
}

Unwind Segue部分:

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (UIView *)findTopMostViewForViewController:(UIViewController *)viewController
{
UIView *theView = viewController.view;
UIViewController *parentViewController = viewController.parentViewController;
while (parentViewController != nil)
{
theView = parentViewController.view;
parentViewController = parentViewController.parentViewController;
}
return theView;
}
- (void)perform
{
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
// Find the views that we will be animating. If the source or destination
// view controller sits inside a container view controller, then the view
// to animate will actually be that parent controller's view.
UIView *sourceView = [self findTopMostViewForViewController:source];
UIView *destinationView = [self findTopMostViewForViewController:destination];
[source dismissViewControllerAnimated:NO completion:^{
// completion code here
}];
}

注 意:Modal Presentation的Unwind Segue很难实现无Bug的任意跳转,因为UIViewController中,跟Container View Controller相关的方法的默认实现并不能很好的定位Container View Controller。而以正确的方式重写这些方法并不容易。所以如果有任意跳转的需求,我们可以尝试自己实现一个简单的Container View Controller。

使用AddChildViewController API实现自己的Container View Controller

我 们偶尔会希望有一个跟Navigation View Controller差不多的容器,但是又不希望像Navigation View Controller那么笨重,且限制多多。我们知道Navigation View Controller在Interface Builder中,其Navigation Bar能容纳的元素样式并不丰富,尽管大多数时候,我们能够通过UIAppearance来定制一些样式,但我们希望定制能容纳更加丰富的元素的 Navigation Bar,或者其他定制的导航界面的时候,希望能够实现一个类似的容器。我们当然可以模仿Navigation View Controller的公开API实现一个差不多的东西,如果我们要很方便的使用自定义Segue和任意跳转的Unwind Segue的话,还需要以特定的方式实现上面提到的一些方法。UIViewController的addChildViewController:方法同 样可以做出类似的功能,而且相比Modal presentation,这种方式代码更加直观。因为使用这个API实现的容器,对子Controller的管理方式与Navigation View Controller类似。

容器的部分代码如下:

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
{
for (UIViewController *childController in self.childViewControllers) {
if ([childController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender]) {
return childController;
}
}
return nil;
}
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier
{
UIStoryboardSegue *unwindSegue = [[MyLeftToRightUnwindSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
return unwindSegue;
}

Segue代码:

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (BOOL)controllerInStack:(UIViewController *)controller
{
UIViewController *fromController = self.sourceViewController;
UIViewController *containerController = fromController.parentViewController;
for (UIViewController *childController in containerController.childViewControllers) {
if (childController == controller) {
return YES;
}
}
return NO;
}
- (void)perform
{
// A simple transition.
// New scene slides in from right and old scene slides out to left.
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
UIViewController *parentController = fromController.parentViewController;
UIView *containerView = parentController.view;
[containerView addSubview:toController.view];
CGRect initialFromRect = fromController.view.frame;
CGRect initialToRect = CGRectOffset(initialFromRect, initialFromRect.size.width, 0);
CGRect finalFromRect = CGRectOffset(initialFromRect, -initialFromRect.size.width, 0);
CGRect finalToRect = initialFromRect;
toController.view.frame = initialToRect;
if (![self controllerInStack:toController]) {
// notify containment event.
[toController willMoveToParentViewController:parentController];
}
[UIView animateWithDuration:0.4f animations:^{
fromController.view.frame = finalFromRect;
toController.view.frame = finalToRect;
} completion:^(BOOL finished) {
if (![self controllerInStack:toController]) {
// Add new controller as a child controller to the container view controller
[parentController addChildViewController:toController];
// notify containment event.
[toController didMoveToParentViewController:toController];
}
[fromController.view removeFromSuperview];
}];
}

Unwind Segue代码:

//
//  CAlayer.m
//  CureFunNew
//
//  Created by Hubery on 2017/9/9.
//  Copyright © 2017年 TLQ. All rights reserved.
//
- (void)perform
{
// A simple transition.
// New scene slides in from left and old scene slides out to right.
UIViewController *fromController = self.sourceViewController;
UIViewController *toController = self.destinationViewController;
UIViewController *parentController = fromController.parentViewController;
UIView *containerView = parentController.view;
[containerView addSubview:toController.view];
CGRect initialFromRect = fromController.view.frame;
CGRect initialToRect = CGRectOffset(initialFromRect, -initialFromRect.size.width, 0);
CGRect finalFromRect = CGRectOffset(initialFromRect, initialFromRect.size.width, 0);
CGRect finalToRect = initialFromRect;
toController.view.frame = initialToRect;
[UIView animateWithDuration:0.4f animations:^{
fromController.view.frame = finalFromRect;
toController.view.frame = finalToRect;
} completion:^(BOOL finished) {
[fromController.view removeFromSuperview];
}];
}

当我们定义的Container View中有需要置顶的元素(比如定制的导航条)时,可以将addSubView:方法换成insertSubView:atIndex:方法来调整子视图的层次。

本次只对转场动画进行了一些归纳总结,后期会对iOS的基础动画和进阶动画进行探究和总结.

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

推荐阅读更多精彩内容