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解释
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;
}
代理方法设置过程类似;
Modal Transition
Modal Transiton 和上面介绍的两种是有区别的,上两个例子里面可以通过containerView 获取当前Transition的容器,并且fromVC和toVC都在容器里面,而在这一点上面Modal是有区别的,在Modal中presentingVC相当于fromVC,presentedVC相当于toVC,两者的视图结构如下
在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
总结:可能上述描述存在错误,欢迎指出,感谢大家!