iOS 转场动画:以Push方式实现Present跳转

在iOS开发中,页面跳转一般有两种方式:

  • navigation:push & pop,动画是从右到左
  • modal(模态):presnet & dismiss,动画是从下到上

在SDK开发中,由于需要减小侵入性,通常会使用modal方式弹出SDK的页面,那么默认唤起的动画就是从下到上。现在有一个需求,需要从右到左唤起SDK的页面,所以我们需要用到转场动画,将present的动画改成从右到左,也就是以Push方式实现Present跳转。

开始前

实现模态的转场动画的步骤,大概分以下几步:

  1. 自定义一个遵循的<UIViewControllerAnimatedTransitioning>协议的动画转场管理对象,并实现两个必须实现的方法:

    1
    2
    3
    4
    //返回动画时间  
    - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
    //所有的转场动画事务都在这个方法里面完成
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;
  2. 成为相应的代理,实现相应的代理方法,返回上面的自定义协议对象

    1
    2
    3
    4
    //返回一个管理prenent动画转场的对象
    - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;
    //返回一个管理dismiss动画转场的对象
    - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;
  3. vc调用presentViewController、dismissViewControllerAnimated时开启 animated

下面按照这个步骤来实现功能

ZDPushAnimatedTransition

创建一个类 ZDPushAnimatedTransition 来封装转场动画

1
2
3
4
5
6
7
8
9
10
11
typedef NS_ENUM(NSUInteger, ZDPushAnimatedTransitionType) {
ZDPushAnimatedTransitionTypePresent, // present动画
ZDPushAnimatedTransitionTypeDismiss // dismiss动画
};

/// push动画
@interface ZDPushAnimatedTransition : NSObject<UIViewControllerAnimatedTransitioning>

- (instancetype)initWithTransitionType:(ZDPushAnimatedTransitionType)type;

@end

实现系统的 UIViewControllerAnimatedTransitioning 协议

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.25;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
switch (_type) {
case ZDPushAnimatedTransitionTypePresent:
[self presentAnimation:transitionContext];
break;
case ZDPushAnimatedTransitionTypeDismiss:
[self dismissAnimation:transitionContext];
break;
}
}

实现present动画,实际就是使用UIView动画来改变 fromVC 和 toVC 的 view 的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
A present to B. fromVC = A, toVC = B
*/
- (void)presentAnimation:(id<UIViewControllerContextTransitioning>)transitionContext {

UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

UIView *containerView = [transitionContext containerView];
[containerView addSubview:fromVC.view];
[containerView addSubview:toVC.view];

toVC.view.frame = CGRectMake(containerView.bounds.size.width, 0, containerView.bounds.size.width, containerView.bounds.size.height);
[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
fromVC.view.transform = CGAffineTransformMakeTranslation(-containerView.bounds.size.width / 3, 0);
toVC.view.transform = CGAffineTransformMakeTranslation(-containerView.bounds.size.width, 0);
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}

实现dismiss动画,和上面的present动画相反,将fromVC 和 toVC 的 view 的位置还原

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
B dismiss to A. fromVC = B, toVC = A
*/
- (void)dismissAnimation:(id<UIViewControllerContextTransitioning>)transitionContext {

UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

UIView *containerView = [transitionContext containerView];
[containerView addSubview:toVC.view];
[containerView addSubview:fromVC.view];

[UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
fromVC.view.transform = CGAffineTransformIdentity;
toVC.view.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}

这样,整个转场动画的封装就完成了,下面来实现代理

实现代理

在要present出来的vc中实现系统代理 UIViewControllerTransitioningDelegate

1
2
3
4
5
6
7
8
9
10
11
12
// 设置代理
self.transitioningDelegate = self;


#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [[ZDPushAnimatedTransition alloc] initWithTransitionType:ZDPushAnimatedTransitionTypePresent];
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return [[ZDPushAnimatedTransition alloc] initWithTransitionType:ZDPushAnimatedTransitionTypeDismiss];
}

这样的话,整个功能就基本实现了

但是,体验上还缺少一点:没有侧滑返回。所以,接下来,我们实现侧滑返回的交互转场

ZDPopInteractiveTransition

创建一个继承自 UIPercentDrivenInteractiveTransition 的类 ZDPopInteractiveTransition 来封装交互转场

1
2
3
4
5
6
7
@interface ZDPopInteractiveTransition : UIPercentDrivenInteractiveTransition

@property (nonatomic, assign) BOOL isTransitioning;

- (instancetype)initWithViewController:(UIViewController *)vc;

@end

为传入的vc添加拖动手势

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (instancetype)initWithViewController:(UIViewController *)vc {
self = [super init];
if (self) {
_viewControllerDismissing = vc;
[self addPanGestureForViewController:viewController];
}
return self;
}

- (void)addPanGestureForViewController:(UIViewController *)vc {
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleGesture:)];
pan.delegate = self;
[vc.view addGestureRecognizer:pan];
}

拖动手势是加在全屏上的,我们需要将它限制到屏幕左侧,这样才是侧滑返回,否则是全屏返回

1
2
3
4
5
6
#pragma mark - UIGestureRecognizerDelegate
/// 侧滑返回
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
return location.x < 50;
}

核心是处理手势交互转场的过程,我们需要更新转场百分比 updateInteractiveTransition ,标记 完成转场 finishInteractiveTransition 和取消转场 cancelInteractiveTransition

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/// 手势过渡的过程
- (void)handleGesture:(UIPanGestureRecognizer *)panGesture {
// 手势百分比
CGFloat transitionX = [panGesture translationInView:panGesture.view].x;
CGFloat percent = transitionX / panGesture.view.frame.size.width;
// 侧滑速度,速度大于某值,判断完成
CGFloat velocityX = [panGesture velocityInView:panGesture.view].x;

switch (panGesture.state) {
case UIGestureRecognizerStateBegan:
// 手势开始的时候标记手势状态,并开始相应的事件
self.isTransitioning = YES;
[self startGesture];
break;
case UIGestureRecognizerStateChanged:
// 手势过程中,通过updateInteractiveTransition设置转场的百分比
[self updateInteractiveTransition:percent];
break;
case UIGestureRecognizerStateEnded:
// 手势完成后结束标记并且判断移动距离是否过半,过则finishInteractiveTransition完成转场操作,否者取消转场操作
self.isTransitioning = NO;
if (percent > 0.3 || velocityX > 300) {
[self finishInteractiveTransition];
} else {
[self cancelInteractiveTransition];
}
break;
default:
break;
}
}

- (void)startGesture {
[_viewControllerDismissing dismissViewControllerAnimated:YES completion:nil];
}

这样,整个交互转场的封装就完成了,下面来实现代理

实现交互代理

在要dismiss消失的vc中实现系统代理 UIViewControllerTransitioningDelegate

1
2
3
4
5
6
7
8
// 创建交互转场对象
self.interactiveDismissing = [[ZDPopInteractiveTransition alloc] initWithViewController:self.visibleViewController];


#pragma mark - UIViewControllerTransitioningDelegate
- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
return _interactiveDismiss.isTransitioning ? _interactiveDismissing : nil;
}

这样的话,整个侧滑返回的功能就实现了

至此,我们基本上就实现了以Push方式实现Present跳转,同时可以像原生导航一样进行侧滑返回


iOS 转场动画:以Push方式实现Present跳转
http://example.com/2021/08/18/iOS-转场动画:以Push方式实现Present跳转/
作者
guanzhendong
发布于
2021年8月18日
许可协议