iOS开发中,页面之间的跳转无外乎由UINavigationController管理的push或者pop操作、以及由UIViewController管理的presentdismiss操作,无论何种操作,iOS原生系统都为我们提供了页面之间的基础跳转动画。但是往往在开发中,由于各种功能需求,iOS原生系统提供的跳转动画并不能满足我们的需求,好在iOS早就给我们提供了一套自定义转场动画的解决方案,这篇文章就来详细了解一下转场动画。在了解这篇文章之前,先看看iOS提供的整个转场框架



present/dismiss

首先,我们先来了解一下模态跳转。开发中,假如在A界面需要模态跳转到B界面,通常会这么写:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
PresentBViewController *bVC = [[PresentBViewController alloc] init];
[self presentViewController:bVC animated:YES completion:nil];
}

模态消失当前界面则是这么写:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:YES completion:nil];
}

UIViewControllerTransitioningDelegate

使用模态跳转时,系统已经为我们写好的跳转的动画,而要想自定义模态跳转动画,则需要一个实现UIViewControllerTransitioningDelegate协议、并实现协议部分方法的对象。在UIViewController.h头文件中,我们可以发现如下定义:

@protocol UIViewControllerTransitioningDelegate;
@interface UIViewController(UIViewControllerTransitioning)
@property (nullable, nonatomic, weak) id <UIViewControllerTransitioningDelegate> transitioningDelegate NS_AVAILABLE_IOS(7_0);
@end

如此,我们则需要定义一个对象,并实现UIViewControllerTransitioningDelegate协议方法,赋值给将要模态跳转的控制器(PresentBViewController),如下:

@interface PresentAViewController ()
@property (nonatomic, strong) PresentManager *presentManager;
@end
@implementation PresentAViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
PresentBViewController *bVC = [[PresentBViewController alloc] init];
self.presentManager = [[PresentManager alloc] init];
bVC.transitioningDelegate = self.presentManager;
[self presentViewController:bVC animated:YES completion:nil];
}
@end

我们先来看看UIViewControllerTransitioningDelegate有哪些协议方法:

//当使用模态弹出时 会调用此方法 返回一个实现 UIViewControllerAnimatedTransitioning 协议的对象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source;
//当使用模态消失时 会调用此方法 返回一个实现 UIViewControllerAnimatedTransitioning 协议的对象
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
// 当使用模态弹出时 会调用此方法 返回一个实现 UIViewControllerInteractiveTransitioning协议的对象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;
// 当使用模态消失时 会调用此方法 返回一个实现 UIViewControllerInteractiveTransitioning协议的对象
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator;
- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented
presentingViewController:(nullable UIViewController *)presenting
sourceViewController:(UIViewController *)source;

先来看这两个代理方法:animationControllerForPresentedController:presentingController:sourceController:(UIViewController *)sourceanimationControllerForDismissedController:(UIViewController *)dismissed他们都返回了一个实现UIViewControllerAnimatedTransitioning协议的对象。UIViewControllerAnimatedTransitioning协议的方法就是我们实现动画的地方。关于此协议我们等会再说。

再来看如下两个方法:
interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animatorinteractionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator,他们都返回了一个实现UIViewControllerInteractiveTransitioning协议的对象,而此协议主要用于交互式转场动画。关于此协议我们等会再说。

push/pop

在使用UINavigationController进行页面之间的导航管理时,系统也是默认为我们实现了pushpop动画的,而如果想自定义push/pop动画,则需要一个实现UINavigationControllerDelegate协议的对象。如下:

@interface PushAViewController ()
@property (nonatomic, strong) PushManager *pushManager;
@end
@implementation PushAViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
PushBViewController *bVC = [[PushBViewController alloc] init];
self.pushManager = [[PushManager alloc] init];
bVC.navigationController.delegate = self.pushManager;
[self.navigationController pushViewController:bVC animated:YES];
}
@end

UINavigationControllerDelegate

我们再来看看UINavigationControllerDelegate协议的相关方法:

//返回真正执行交互式动画的对象
- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>) animationController;
//返回由谁去执行动画的对象
- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController *)fromVC
toViewController:(UIViewController *)toVC;

我们会发现 此两种方法分别返回了实现了UIViewControllerInteractiveTransitioning协议的对象和实现了UIViewControllerAnimatedTransitioning协议的对象。我们不难发现,无论是push/pop还是present/dismiss真正的执行动画都是一个实现了UIViewControllerAnimatedTransitioning协议的对象,而真正执行交互式动画的都是一个实现了UIViewControllerInteractiveTransitioning协议的对象。既然如此,那我们就来研究一下UIViewControllerAnimatedTransitioningUIViewControllerInteractiveTransitioning协议

UIViewControllerAnimatedTransitioning

先来看此协议的方法:

//动画执行的时间
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
//执行动画
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

此协议的两个方法必须实现。transitionDuration:方法返回动画执行的时间。通常情况下,系统执行present/dismisspush/pop动画的时间为0.5秒左右。animateTransition方法就是真正执行动画的地方。这个方法系统会给我们传过来一个实现了UIViewControllerContextTransitioning协议的对象(转场上下文),在执行动画之前,我们先来了解一下UIViewControllerContextTransitioning协议。

先来看UIViewControllerContextTransitioning协议的定义

//上下文的view 对做动画的view需要添加到此上下文的view中
- (UIView *)containerView;
//是否动画正在进行
- (BOOL)isAnimated;
//是否是交互式动画
- (BOOL)isInteractive;
//是否取消
- (BOOL)transitionWasCancelled;
//获取当前模态跳转的方式
- (UIModalPresentationStyle)presentationStyle;
//根据系数来更新交互式动画 0~1
- (void)updateInteractiveTransition:(CGFloat)percentComplete;
//完成交互式动画
- (void)finishInteractiveTransition;
//取消交互式动画
- (void)cancelInteractiveTransition;
//iOS10新加入的方法 暂停交互式动画
- (void)pauseInteractiveTransition NS_AVAILABLE_IOS(10_0);
//当动画结束时 要调用此方法告诉上下文
- (void)completeTransition:(BOOL)didComplete;
//获取当前上下文的控制器,使用UITransitionContextFromViewControllerKey获取fromVC 使用UITransitionContextToViewControllerKey获取toVC
- (nullable __kindof UIViewController *)viewControllerForKey:(UITransitionContextViewControllerKey)key;
//iOS8之后的方法 返回此上下文控制器的view ,通常情况下,尽可能不直接使用controller的view属性,因为有时候我们直接使用controller的view并不是我们真正要做动画的view。我们应该直接使用UITransitionContextFromViewKey来获取fromView,使用UITransitionContextToViewKey来获取toView。
- (nullable __kindof UIView *)viewForKey:(UITransitionContextViewKey)key NS_AVAILABLE_IOS(8_0);
//目标view的transform
- (CGAffineTransform)targetTransform NS_AVAILABLE_IOS(8_0);
//返回初始位置的frame 即fromView的frame
- (CGRect)initialFrameForViewController:(UIViewController *)vc;
//返回动画结束位置的frame 即toView得frame
- (CGRect)finalFrameForViewController:(UIViewController *)vc;

了解完UIViewControllerContextTransitioning协议后,我们就可以实现自定义转场动画了。

第一节中,我们定义了类PresentManager并实现了UIViewControllerAnimatedTransitioningUIViewControllerTransitioningDelegate,我们先来看UIViewControllerTransitioningDelegate协议的具体实现

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
//标记此动画为Present 并使用自身为动画的最终执行者
self.transitionStyle = TransitionStylePresent;
return self;
}
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
//标记此动画为Dismiss 并使用自身为动画的最终执行者
self.transitionStyle = TransitionStyleDismiss;
return self;
}

再来看看UIViewControllerAnimatedTransitioning协议的具体实现:

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
//返回动画的执行时间
return 1.0;
}
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
//1. 首先获取上下文的view
UIView *containerView = [transitionContext containerView];
if (!containerView) {
return;
}
if (self.transitionStyle == TransitionStylePresent) {
//present 动画
//2. 获取 fromViewController 和 toViewController
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//3. 获取fromView和toView
UIView *fromView, *toView;
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
toView = [transitionContext viewForKey:UITransitionContextToViewKey];
}else {
fromView = fromViewController.view;
toView = toViewController.view;
}
fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
toView.frame = [transitionContext finalFrameForViewController:toViewController];
//4. 设置toView动画初始frame 这里 模仿一下从屏幕左边模态跳转
toView.frame = CGRectMake(-toView.frame.size.width, toView.frame.origin.y, toView.frame.size.width, toView.frame.size.height);
//5. 添加到上下文的view上
[containerView addSubview:fromView];
[containerView addSubview:toView];
//6. 执行动画
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
toView.frame = CGRectMake(0, toView.frame.origin.y, toView.frame.size.width, toView.frame.size.height);
} completion:^(BOOL finished) {
//7. 一定要告诉上下文 动画执行结束
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}else {
// pop动画
//2. 获取 fromViewController 和 toViewController
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
//3. 获取fromView和toView
UIView *fromView, *toView;
if ([transitionContext respondsToSelector:@selector(viewForKey:)]) {
fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
toView = [transitionContext viewForKey:UITransitionContextToViewKey];
}else {
fromView = fromViewController.view;
toView = toViewController.view;
}
fromView.frame = [transitionContext initialFrameForViewController:fromViewController];
toView.frame = [transitionContext finalFrameForViewController:toViewController];
//4. 添加到上下文的view上
[containerView addSubview:toView];
[containerView addSubview:fromView];
//5. 执行动画
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
fromView.frame = CGRectMake(0, -fromView.frame.size.height, fromView.frame.size.width, fromView.frame.size.height);
} completion:^(BOOL finished) {
//7. 一定要告诉上下文 动画执行结束
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
}

我这里只是做一个简单的动画,至于该如何做更复杂的动画,那就开动大家的大脑吧。

同样的,自定义push/pop动画也是如此,这里不再详解。

交互式动画

在前面,我们一直提到一个协议UIViewControllerInteractiveTransitioning,实现此协议,我们就能使用交互式转场动画。iOS7为我们提供了已经实现好此协议的类UIPercentDrivenInteractiveTransition,我们只需继承此类,便可实现交互式动画。为了方便,下面的例子依旧在present/dismiss转场基础上讲解,我们让PresentManager继承UIPercentDrivenInteractiveTransition,并且在PresentBViewController上添加pan手势,具体代码如下:

//这里为了方便 当A控制器present B控制器的时候 直接在B控制器上添加pan手势,并且强引用B控制器。
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented
presentingController:(UIViewController *)presenting
sourceController:(UIViewController *)source {
//标记此动画为Present 并使用自身为动画的最终执行者
self.transitionStyle = TransitionStylePresent;
//添加手势
[self addGesture:presented];
self.presentingVC = presented;
return self;
}
//在代理方法中返回`PresentManager`自己作为交互式代理
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
if (self.interacting) {
return self;
}
return nil;
}
//添加手势
- (void)addGesture:(UIViewController *)viewController {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGesture:)];
[viewController.view addGestureRecognizer:pan];
}
//监听手势变化
- (void)panGesture:(UIPanGestureRecognizer *)gesture {
CGPoint translation = [gesture translationInView:gesture.view];
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
//标记当前模态消失为手势调用
self.interacting = YES;
[self.presentingVC dismissViewControllerAnimated:YES completion:nil];
break;
case UIGestureRecognizerStateChanged: {
//根据手势的滑动距离 更新状态
CGFloat fraction = translation.x / gesture.view.frame.size.width;
[self updateInteractiveTransition:fraction];
break;
}
case UIGestureRecognizerStateEnded:
case UIGestureRecognizerStateCancelled: {
//如果划过50% dismiss掉 否则恢复
self.interacting = NO;
CGFloat fraction = translation.x / gesture.view.frame.size.width;
if (fraction<0.5 || gesture.state == UIGestureRecognizerStateCancelled) {
[self cancelInteractiveTransition];
} else {
[self finishInteractiveTransition];
}
break;
}
default:
break;
}
}

同样的,push/pop亦是如此。

UIPresentationController

在第一节,我们说到UIViewControllerTransitioningDelegate协议的时候,还有个方法没有说,它是iOS8以后才有的,此方法为:

- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented
presentingViewController:(nullable UIViewController *)presenting
sourceViewController:(UIViewController *)source

此方法返回一个UIPresentationController对象,这个对象是做什么的呢?

UIViewController有一个属性modalPresentationStyle,我们来看看它有哪些值:

typedef NS_ENUM(NSInteger, UIModalPresentationStyle) {
UIModalPresentationFullScreen = 0,
UIModalPresentationPageSheet NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
UIModalPresentationFormSheet NS_ENUM_AVAILABLE_IOS(3_2) __TVOS_PROHIBITED,
UIModalPresentationCurrentContext NS_ENUM_AVAILABLE_IOS(3_2),
UIModalPresentationCustom NS_ENUM_AVAILABLE_IOS(7_0),
UIModalPresentationOverFullScreen NS_ENUM_AVAILABLE_IOS(8_0),
UIModalPresentationOverCurrentContext NS_ENUM_AVAILABLE_IOS(8_0),
UIModalPresentationPopover NS_ENUM_AVAILABLE_IOS(8_0) __TVOS_PROHIBITED,
UIModalPresentationNone NS_ENUM_AVAILABLE_IOS(7_0) = -1,
};

平常开发中,我们使用最多的就是UIModalPresentationCustom,如果我们不使用UIModalPresentationCustom,默认的系统会在我们调用“上下文”的completeTransition方法后会把fromVC移除掉。

如果我们想在present/pop执行动画的生命周期过程中,任意的在上下文中插入视图或者更改最终视图的大小等,使用UIPresentationController便可实现。我们来看UIPresentationController类的定义:

//上下文的view
@property(nullable, nonatomic, readonly, strong) UIView *containerView;
//即将布局
- (void)containerViewWillLayoutSubviews;
//正在布局
- (void)containerViewDidLayoutSubviews;
//返回模态跳转后的view(B控制器View)的最终frame 通常情况下要重写此方法
- (CGRect)frameOfPresentedViewInContainerView;
//周期方法
- (void)presentationTransitionWillBegin;
- (void)presentationTransitionDidEnd:(BOOL)completed;
- (void)dismissalTransitionWillBegin;
- (void)dismissalTransitionDidEnd:(BOOL)completed;

如此,我们便可在视图周期方法中任意的添加和删除视图,以满足我们的需求。也可以在布局过程中,改变弹出视图的frame。切记,视图的添加和删除都是在上下文的view中进行的。

小结

了解完整个转场动画的框架,我们合理的使用框架中的协议和类,便能尽可能的满足我们的开发需求。至于该如何实现,大家可以动脑了~