ViewController Transition

作者: coder_feng | 来源:发表于2017-01-17 08:30 被阅读3014次

    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

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

    相关文章

      网友评论

      本文标题:ViewController Transition

      本文链接:https://www.haomeiwen.com/subject/ebcclttx.html