ViewController Transition

iOS视图控制器详解

视图控制器中的视图显示在屏幕上有两种方式:最主要的方式是内嵌在容器控制器中,比如 UINavigationController,UITabBarController, UISplitController;由另外一个视图控制器显示它,这种方式通常被称为模态(Modal)显示;具体方式是在 NavigationController 里 push 或 pop 一个 View Controller,在 TabBarController 中切换到其他 View Controller,以 Modal 方式显示另外一个 View Controller,这些都是 View Controller Transition。在 storyboard 里,每个 View Controller 是一个 Scene,View Controller Transition 便是从一个 Scene 转换到另外一个 Scene;官网链接View Controller Programming Guide for iOS

触发转场的方式

目前为止,官方支持以下几种transition方式

1.在 UINavigationController 中 push 和 pop;

2.在 UITabBarController 中切换 Tab;

3.Modal transition:presentation 和 dismissal ,称为视图控制器的模态显示和消失,但是它的model类型属性modalPresentationStyle 只能限定在UIModalPresentationFullScreen 或 UIModalPresentationCustom 这两种模式;

以上三种transition都需要代理和动画控制器才可以实现自定义动画,触发的方式分为三种

1)代码里调用相关动作的方法

2)Segue

3)容器 VC,在 UINavigationBar 和 UITabBar 上的相关 Item 的点击操作

相关动作方法

UINavigationController 中所有修改其viewControllers栈中 VC 的方法,就可以自定义transition动画:

下面分别是push和pop方法

- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated;

- (nullable UIViewController *)popViewControllerAnimated:(BOOL)animated;

- (void)setViewControllers:(NSArray*)viewControllers animated:(BOOL)animate;//这个方法是对VC栈的整体更新

- (nullable NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated

UITabBarController

@property(nullable, nonatomic, assign) __kindof UIViewController *selectedViewController;//传递的参数必须是其下的子VC

@property(nonatomic) NSUInteger selectedIndex;//选中控制器的索引

- (void)setViewControllers:(NSArray<__kindof UIViewController *> * __nullable)viewControllers animated:(BOOL)animated;//和上面的差不多意思

Modal

- (void)presentViewController:(UIViewController *)viewControllerToPresent animated: (BOOL)flag completion:(void (^ __nullable)(void))completion NS_AVAILABLE_IOS(5_0);//Presentation

- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion NS_AVAILABLE_IOS(5_0);//dismiss

Segue

这种方式是在storyboard里面设置的:存在两种方式(transition发生前修改转场参数的最后机会)

performSegueWithIdentifier:sender:

prepareForSegue:sender

Transition解释

WWDC 2013 Session 218

transition过程之中,作为容器的父 VC 维护着多个子 VC,但在视图结构上,只保留一个子 VC 的视图,所以转场的本质是下一场景(子 VC)的视图替换当前场景(子 VC)的视图以及相应的控制器(子 VC)的替换,表现为当前视图消失和下一视图出现,基于此进行动画,动画的方式非常多;

iOS 7 以协议的方式开放了自定义转场的 API,协议的好处是不再拘泥于具体的某个类,只要是遵守该协议的对象都能参与转场,非常灵活。转场协议由5种协议组成,在实际中只需要我们提供其中的两个或三个便能实现绝大部分的transiton动画:

1.Transition代理(Transition Delegate):

实现自定义Transition的第一步就是提供代理,使用我们自己提供的代理,而不是系统默认的的代理

//UINavigationController 的 delegate 属性遵守该协议。

//UITabBarController 的 delegate 属性遵守该协议。

//UIViewController 的 transitioningDelegate 属性遵守该协议。(iOS7新增的)

Transition发生时候,UIKit要求代理提供transition动画的构件:动画控制器和交互控制器(可选的)

2.动画控制器(Animation Controller)

负责添加视图与及执行动画:遵守<UIViewControllerAnimatedTransitioning>协议

3.交互控制器(Interaction Controller)

通过交互手段,来控制动画,遵守<UIViewControllerInteractiveTransitioning>协议;

4.Transition 上下文(Transition Context)

提供Transition过程中需要的数据;遵守<UIViewControllerContextTransitioning>协议;

5.Transition 协调器(Transition Coordinator)

可以在Transition动画发生的同时执行其他动画;遵守<UIViewControllerTransitionCoordinator>协议,在IOS7中新增了方法transitionCoordinator()返回一个遵守协议的对象,并且该方法只在控制器Transition 的过程中才返回一个类对象;否则返回nil

非交互Transition

这个阶段需要做两件事,提供Transition代理,并由代理提供动画控制器(交互控制器和动画控制器是可选实现的),没有实现或者返回ni的话则使用默认的Transition效果。总的来说,动画控制器是表现的核心部分,代理方法也非常简单,让我们先从动画控制器入手;

动画控制器协议

动画控制器负责添加视图以及执行动画,遵守UIViewControllerAnimatedTransitioning协议,该协议要求实现以下方法:

-(void)animateTransition:(id)transitionContext;//执行动画的地方,最核心的方法。

-(NSTimeInterval)transitionDuration:(id)transitionContext;//返回动画时间

-(void)animationEnded:(BOOL)transitionCompleted;////如果实现了,会在转场动画结束后调用,可以执行一些收尾工作。

最重要是第一个方法,遵守<UIViewControllerContextTransitioning>协议的transition context对象,提供需要的重要数据,参与视图控制器和transition过程的状态信息

可以通过transitionContext在-(void)animateTransition:(id)transitionContext 这个方法里面来获取动画控制器需要的重要信息,例如根视图,根控制器,方法如下:

- (UIView *)containerView; //返回容器视图,获取Transition动画发生的地方;

- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;//获取视图控制器(通过UITransitionContextFromViewControllerKey,UITransitionContextToViewControllerKey这两个key)

- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key NS_AVAILABLE_IOS(8_0); //8.0之后诞生的方法来获取根控制器的视图

@note:通过viewForKey获取的视图就是viewControllerForKey返回控制器的根视图,或者nil(为nil的情况只有在Modal的Transition中才会出现,因为containner中的view不包含presentingView),获取view的方法如下

UIView *containView = [transitionContext containerView];

UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

UIView *fromView = fromVC.view;

UIView *toView = toVC.view;

上面的fromView和toView其实是和viewForKey中相应key值获取的是一样的

Transition的本质是下一个Scene替换当前的Scene,从当前过渡到下一个;

fromView:即将消失或者被替换的视图,对应的控制器是FromVC;

toView:即将显示或者要替换的视图,对应的控制器是toVC;

导航控制器(UINavigationController)的Transition

demo中采取的方法是单独生成一个代理类,这个类遵守<UINavigationControllerDelegate>

-(id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {NSUInteger transitionType = operation;

SlideAnimationController *slide = [[SlideAnimationController alloc]init];

slide.transitionType = transitionType;

return slide;

}  //该方法返回动画控制器,若返回来的是nil则使用系统默认的效果



@note:改类中实现代理的方法采用的是storyBoard里面的Object对象设置的,如果你使用的是self.navigationController.delegate = [xxxx new],那么在初始化,离开该方法之后,delegate将重新变为nil,然后就不会再调用代理方法,原因是因为delegate是弱引用,如果你不采取在storyBoard里面设置的话,你可以通过一个本地变量来达到强引用的效果,但是设置的时候应该也要小心,viewDidload方法设置的时候有坑,有可能控制器self.navigationController = nil,设置出来的self.navigationController.delegate 肯定也是nil,所以建议在prepareForSegue:sender: 这里设置比较好;

Demo地址:NavigationControllerTransition

TabBar导航控制器(UITabBarController)

UITabBarController 的转场代理和 UINavigationController 类似,都是通过动画控制器提供相应的方法完成,demo中的lei遵守<UITabBarControllerDelegate>协议,但是该协议里面并没有提供滑动方向的相关方法,需要我们根据相关属性来判断;

-(id)tabBarController:(UITabBarController *)tabBarController animationControllerForTransitionFromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {CGFloat fromIndex = [tabBarController.viewControllers indexOfObject:fromVC];

CGFloat toIndex = [tabBarController.viewControllers indexOfObject:toVC];

NSUInteger tabChangeDirection = toIndex < fromIndex ? TransitionTypeLeft : TransitionTypeRight;

SlideAnimationController *slideAnimationController = [[SlideAnimationController alloc]init];

slideAnimationController.transitionType = tabChangeDirection;

return slideAnimationController;

}

代理方法设置过程类似;

Demo:SLScrollTabController

Modal Transition

Modal Transiton 和上面介绍的两种是有区别的,上两个例子里面可以通过containerView 获取当前Transition的容器,并且fromVC和toVC都在容器里面,而在这一点上面Modal是有区别的,在Modal中presentingVC相当于fromVC,presentedVC相当于toVC,两者的视图结构如下


ContainerVC VS Modal


在Modal Transition里面,我们着重讲UIModalPresentationFullScreen模式和UIModalPresentationCustom模式,这两种在modal transition中的机制又是不一样的,UIModalPresentationFullScreen 模式下,Modal Transiton结束后 fromView 依然主动被从视图结构中移除了,但是UIModalPresentationCustom没有移除,就是因为这个区别导致处理dismissal的时候容易出现问题;

dismissal Transition 场景

1.FullScreen 模式:presentation 结束后,presentingView 被主动移出视图结构,不过,在 dismissal transition中希望其出现在屏幕上并且在对其添加动画怎么办呢?实际上,你按照容器类 VC 转场里动画控制器里那样做也没有问题,就是将其加入 containerView 并添加动画。不用担心,结束后,UIKit 会自动将其恢复到原来的位置。

2.Custom 模式:presentation 结束后,presentingView(fromView) 未被主动移出视图结构,在 dismissal 中,注意不要像其他转场中那样将 presentingView(toView) 加入 containerView 中,否则 dismissal 结束后本来可见的 presentingView 将会随着 containerView 一起被移除。如果你在 Custom 模式下没有注意到这点,很容易出现黑屏之类的现象而不知道问题所在。

虽然ios8以上的系统,可以通过UIPresentationController类并重写以下方法并返回true可以解决上述问题:

// Indicate whether the view controller's view we are transitioning from will be removed from the window in the end of the presentation transition  (Default: NO)

- (BOOL)shouldRemovePresentersView

@note UIPresentationController 类的作用并没有改变上面所说的presentingview和containerView的层次关系,但是能修复这个问题,应该是返回YES之后,同时对两个视图进行控制

结论:尽量不要在Modal Transiton中的custome模式中对presentingView进行动画,并且针对custom模式下的使用,官文文档比较详细Creating Custom Presenting

Demo:SLCustomModalTransitionDemo

交互式Transition

在非交互Transition的基础之上将之交互需要两个条件

1.由转场代理提供交互控制器,这是一个遵守协议的对象,不过系统已经打包好了现成的类UIPercentDrivenInteractiveTransition供我们使用。我们不需要做任何配置,仅仅在Transition代理的相应方法中提供一个该类实例便能工作。另外交互控制器必须有动画控制器才能工作。

2.交互控制器需要交互手段的配合,常用的是使用手势;


如果在转场代理中提供了交互控制器,而转场发生时并没有方法来驱动转场进程(比如手势),转场过程将一直处于开始阶段无法结束,应用界面也会失去响应:在 NavigationController 中点击 NavigationBar 也能实现 pop 返回操作,但此时没有了交互手段的支持,Transition过程卡;可以通过一个变量来标记交互状态,该变量由交互手势来更新状态

-(instancetype)init {  

  if (self = [super init]) {     

   _interactive = false;        _interactionController = [[UIPercentDrivenInteractiveTransition alloc]init];

    }    

return self;

}

-(id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC {

    NSUInteger transitionType = operation;   

 SlideAnimationController *slide = [[SlideAnimationController alloc]init];    

slide.transitionType = transitionType;   

 return slide;

}-(id)navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id)animationController {

return _interactive ? _interactionController : nil;

}

TabBarController的实现也类似;系统打包好的UIPercentDrivenInteractiveTransition中的控制转场进度的方法与转场环境对象提供的三个方法同名,实际上只是前者调用了后者的方法而已。系统以一种解耦的方式使得动画控制器,交互控制器,转场环境对象互相协作,我们只需要使用UIPercentDrivenInteractiveTransition的三个同名方法来控制进度就够了。如果你要实现自己的交互控制器,而不是UIPercentDrivenInteractiveTransition的子类,就需要调用转场环境的三个方法来控制进度;

交互手段

在上面NavigationControllerTransition 的demo中的Slide控制器提供动画来实现右滑返回的效果,绑定方法如下

-(void)handleEdgePanGesture:(UIScreenEdgePanGestureRecognizer *)gesture {

CGFloat translationX = [gesture translationInView:self.view].x;

CGFloat translationBase = self.view.frame.size.width / 3;

CGFloat translationAbs = translationX > 0 ? translationX : -translationX;

CGFloat percent = translationAbs > translationBase ? 1.0 : translationAbs / translationBase; //根据移动距离计算交互过程的进度。

switch (gesture.state) {

case UIGestureRecognizerStateBegan:

_navigationDelegate = self.navigationController.delegate;////转场开始前获取代理,一旦转场开始,VC 将脱离控制器栈,此后 self.navigationController 返回的是 nil。

_navigationDelegate.interactive = true;//更新交互状态

[self.navigationController popViewControllerAnimated:YES];//.如果转场代理提供了交互控制器,它将从这时候开始接管转场过程。

break;

case UIGestureRecognizerStateChanged:

//更新转场进度,进度数值范围为0.0~1.0。

[_navigationDelegate.interactionController updateInteractiveTransition:percent];//.更新进度:

break;

case UIGestureRecognizerStateCancelled:

case UIGestureRecognizerStateEnded:

if (percent > 0.5) {//.结束转场:

//完成转场,转场动画从当前状态继续直至结束。(转场动画从当前的状态将继续进行直到动画结束,转场完成)

[_navigationDelegate.interactionController finishInteractiveTransition];////完成转场。

}else {

//取消转场,转场动画从当前状态返回至转场发生前的状态。(被调用后,转场动画从当前的状态回拨到初始状态,转场取消。)

[_navigationDelegate.interactionController cancelInteractiveTransition];////或者,取消转场。

}

_navigationDelegate.interactive = false;//无论转场的结果如何,恢复为非交互状态。

break;

default:

break;

}

}


@note:众所周知,app的生命周期是按照一定顺序的,但是介入了Transiton的时候,顺序就得不到保证了,本来正确的生命周期应该是如下的:

1.viewWillAppear 2.viewDidAppear 3.viewWillDisappear 4.viewDidDisappear

介入Transition之后,顺序变得错综复杂,可以参考这个链接查看相关情况The Inconsistent Order of View Transition Events

总结:综合前面所讲的内容,都还没有办法实现Transition中的任意阶段的中断,并执行新的动画;但是通过下面介绍的Transition Coordinator就可以实现了这种效果

Transition Coordinator

Transition Coordinator使用的时间比较少,但是它可以在Transition过程中的任意阶段搜集动作并在交互中执行;

Modal Transition中UIPresentationController类只能通过转场协调器来与动画控制器同步,并行执行其他动画;

使用这两个方法

- (BOOL)animateAlongsideTransition:(void (^ __nullable)(idcontext))animation                        completion:(void (^ __nullable)(idcontext))completion;// This alternative API is needed if the view is not a descendent of the container view AND you require this animation// to be driven by a UIPercentDrivenInteractiveTransition interaction controller.- (BOOL)animateAlongsideTransitionInView:(nullable UIView *)view                              animation:(void (^ __nullable)(idcontext))animation                              completion:(void (^ __nullable)(idcontext))completion;

这里它可以在交互式转场结束时执行一个闭包

// When a transition changes from interactive to non-interactive then handler is// invoked. The handler will typically then do something depending on whether or// not the transition isCancelled. Note that only interactive transitions can// be cancelled and all interactive transitions complete as non-interactive// ones. In general, when a transition is cancelled the view controller that was// appearing will receive a viewWillDisappear: call, and the view controller// that was disappearing will receive a viewWillAppear: call.  This handler is// invoked BEFORE the "will" method calls are made.- (void)notifyWhenInteractionEndsUsingBlock: (void (^)(idcontext))handler NS_DEPRECATED_IOS(7_0, 10_0,"Use notifyWhenInteractionChangesUsingBlock");

当Transition由交互状态转变为非交互状态(在手势交互过程中则为手势结束时),无论Transition的结果是完成还是被取消,该方法都会被调用;得益于闭包,Transition协调器可以在转场过程中的任意阶段搜集动作并在交互中止后执行。闭包中的参数是一个遵守协议的对象,该对象由 UIKit 提供,和前面的Transition环境对象作用类似;另外交互状态结束时并非Transition过程的终点(此后动画控制器提供的Transition动画根据交互结束时的状态继续或是返回到初始状态),而是由动画控制器来结束这一切:

- (void)animationEnded:(BOOL) transitionCompleted;


向非交互阶段的平滑过渡

这部分的功能没有实现过,但是找资料发现使用UIViewControllerInteractiveTransitioning协议定义了两个属性可以做到平滑过渡

completionCurve //交互结束后剩余动画的速率曲线

completionSpeed //交互结束后动画的开始速率由该参数与原来的速率相乘得到,实际上是个缩放参数,这里应该使用单位变化速率(即你要的速率/距离)。注意:completionSpeed会影响剩余的动画时间,而不是之前设定的转场动画时间剩下的时间;当completionSpeed很小时剩余的动画时间可能会被拉伸得很长,所以过滤下较低的速率比较好。如果不设置两个参数,转场动画将以原来的速率曲线在当前进度的速率继续。不过从实际使用效果来看,往往不到0.5s的动画时间,基本上看不出什么效果来。


iOS10全程交互控制

在Transition动画里,非交互Transition与交互Transition之间有着明显的界限:如果以交互转场开始,尽管在交互结束后会切换到动画过程,但之后无法再次切换到交互过程,只能等待其结束;如果以非交互Transition开始,在动画结束前是无法切换到交互过程的,只能等待其结束,但是在2016年的WWDC上面介绍的iOS10打破了这个局面,相关链接WWDC 2016 Session216:Advances in UIKit Animations And Transitions

让转场动画在非交互状态与交互状态之间自由切换很困难,UIViewPropertyAnimator类实现了需要的所有基础功能,使得难度降低了许多,Demo中使用一个UIViewPropertyAnimator对象,就可以实现转场动画的全程交互控制,甚至不需交互控制器;下面展示Demo中来实现Push和Pop过程全程交互控制的几个重要方法:

// 提供一个 UIViewPropertyAnimator,由它来执行转场动画以及实现交互控制

[animator pauseAnimation];动画暂停

[animator setReversed:true]; 动画反向

[animator startAnimation];开始动画

_fractionComplete = animator.fractionComplete; 更新Transition进度

[animator continueAnimationWithTimingParameters:[[UISpringTimingParameters alloc]initWithDampingRatio:0.9 initialVelocity:initialVelocity] durationFactor:0]; 手指离开屏幕的速度继续执行剩下的Transition动画,保障动画协调过渡

Demo:PushAndPop


总结:可能上述描述存在错误,欢迎指出,感谢大家!

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

推荐阅读更多精彩内容