源码

iOS自定义转场动画/UIPresentationController

一.UIPresentationController简介

UIPresentationController是 iOS8 新增的一个API,苹果的官方定义是:对象为所呈现的视图控制器提供高级视图的转换管理(从呈现视图控制器的时间直到它被消除期间)。其实说白了就是用来控制controller之间的跳转特效。比如希望实现一个特效,显示一个窗口,大小和位置都是自定义的,并且遮罩在原来的页面上。

下图是苹果官方自定义modal动画的栗子,点我直接下载

苹果官方Demo

网上找到的UIPresentationController在自定义modal动画中的位置:

二.UIPresentationController作用

  • 管理所有Modal出来的控制器

  • 管理/监听 切换控制器的过程

  • 控制器一旦调了presentViewController方法,控制器的presentationController,会先创建好了,然后整个控制器的切换由presentationController管理。如下代码:

AViewController *vc = [[AViewController alloc] init];
[self presentViewController:vc animated:YES completion:nil];


三.UIPresentationController方法和属性介绍

/**
 构造方法,苹果建议使用这个初始化UIPresentationController
 @param presentedViewController 将要跳转到的目标控制器
 @param presentingViewController 跳转前的原控制器
 */

- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController;

presentedViewController:     要 modal 显示的视图控制器
presentingViewController:    跳转前视图控制器
containerView()              容器视图
presentedView()              被展现控制器的视图

func presentationTransitionWillBegin()          跳转将要开始
func presentationTransitionDidEnd(completed: Bool)  跳转完成
func dismissalTransitionWillBegin()         dismiss将要开始
func dismissalTransitionDidEnd(completed: Bool)     dismiss完成
func frameOfPresentedViewInContainerView()    动画之后,目标控制器View的位置

四.自定义modal转场动画的第三个步骤

第一步.写一个遵守UIViewControllerTransitioningDelegate协议的类,来告诉控制器,谁是动画主管(UIPresentationController),谁是开始动画的具体细节负责类、谁是结束动画的具体细节负责类。

第二步.写一个UIPresentationController的子类(动画主管) --> 负责「被呈现」及「负责呈现」的controller以外的controller,比如带渐变效果的黑色半透明背景View。在此步骤,起码需要重写以下5个方法:

  1. presentationTransitionWillBegin

  2. presentationTransitionDidEnd:

  3. dismissalTransitionWillBegin

  4. dismissalTransitionDidEnd:

  5. frameOfPresentedViewInContainerView

第三步.写一个遵守UIViewControllerAnimatedTransitioning协议的类,负责动画细节。比如怎么出现,位置在哪,动画细节如何等。

下面将通过几个Demo,来实现自定义转场动画,建议直接下载Demo运行看效果,gif图有点失真失帧。

Demo下载地址: 点我下载

Demo效果图

五.自定义modal转场动画第一个Demo

第一个Demo比较简单,不用我们自己去写动画效果,这里将贴出部分代码,后续Demo将不贴代码了,可以自己去下载Demo看看,里面加了很详细的注释。

按照上面所写的步骤,为了方便,我们将第一步、第二步合并在一起,使用一个类同时实现

UIViewControllerTransitioningDelegate协议,并继承UIPresentationController

h文件的内容:

#import 
/**
 * 实现自定义过渡动画:
 * 1.继承UIPresentationController 成为子类
 * 2.遵守UIViewControllerAnimatedTransitioning 协议
 * 其实也可以写成两个类,分别继承UIPresentationController和实现UIViewControllerAnimatedTransitioning协议
 */

@interface BCustomPresentationController : UIPresentationController <UIViewControllerTransitioningDelegate>
/** 黑色半透明背景 */
@property (nonatomicstrongUIView *dimmingView;
@end

m文件的内容:

@implementation BCustomPresentationController

//| ------------------------------第一步内容----------------------------------------------
#pragma mark - UIViewControllerTransitioningDelegate
/*
 * 来告诉控制器,谁是动画主管(UIPresentationController),因为此类继承了UIPresentationController,就返回了self
 */

- (UIPresentationController* )presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source {
    return self;
}

//| ------------------------------第二步内容----------------------------------------------
#pragma mark - 重写UIPresentationController个别方法
- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController {
    self = [super initWithPresentedViewController:presentedViewController presentingViewController:presentingViewController];
    if (self) {
        // 必须设置 presentedViewController 的 modalPresentationStyle
        // 在自定义动画效果的情况下,苹果强烈建议设置为 UIModalPresentationCustom
        presentedViewController.modalPresentationStyle = UIModalPresentationCustom;
    }
    return self;
}

// 呈现过渡即将开始的时候被调用的
// 可以在此方法创建和设置自定义动画所需的view
- (void)presentationTransitionWillBegin {
    // 背景遮罩
    UIView *dimmingView = [[UIView alloc] initWithFrame:self.containerView.bounds];
    dimmingView.backgroundColor = [UIColor blackColor];
    dimmingView.opaque = NO//是否透明
    dimmingView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    [dimmingView addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dimmingViewTapped:)]];
    self.dimmingView = dimmingView;

    [self.containerView addSubview:dimmingView]; // 添加到动画容器View中。

    // 获取presentingViewController 的转换协调器,应该动画期间的一个类?上下文?之类的,负责动画的一个东西
    id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;

    // 动画期间,背景View的动画方式
    self.dimmingView.alpha = 0.f;
    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.dimmingView.alpha = 0.4f;
    } completion:NULL];
}

#pragma mark 点击了背景遮罩view
- (void)dimmingViewTapped:(UITapGestureRecognizer*)sender {
    [self.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
}

// 在呈现过渡结束时被调用的,并且该方法提供一个布尔变量来判断过渡效果是否完成
- (void)presentationTransitionDidEnd:(BOOL)completed {
    // 在取消动画的情况下,可能为NO,这种情况下,应该取消视图的引用,防止视图没有释放
    if (!completed) {
        self.dimmingView = nil;
    }
}

// 消失过渡即将开始的时候被调用的
- (void)dismissalTransitionWillBegin {
    id<UIViewControllerTransitionCoordinator> transitionCoordinator = self.presentingViewController.transitionCoordinator;

    [transitionCoordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.dimmingView.alpha = 0.f;
    } completion:NULL];
}

// 消失过渡完成之后调用,此时应该将视图移除,防止强引用
- (void)dismissalTransitionDidEnd:(BOOL)completed {
    if (completed == YES) {
        [self.dimmingView removeFromSuperview];
        self.dimmingView = nil;
    }
}

// 返回目标控制器Viewframe
- (CGRect)frameOfPresentedViewInContainerView {
    // 这里直接按照想要的大小写死,其实这样写不好,在第二个Demo里,我们将按照苹果官方Demo,写灵活的获取方式。
    CGFloat height = 300.f;

    CGRect containerViewBounds = self.containerView.bounds;
    containerViewBounds.origin.y = containerViewBounds.size.height - height;
    containerViewBounds.size.height = height;
    return containerViewBounds;
}

//  建议就这样重写就行,这个应该是控制器内容大小变化时,就会调用这个方法, 比如适配横竖屏幕时,翻转屏幕时
//  可以使用UIContentContainer的方法来调整任何子视图控制器的大小或位置。
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
    [super preferredContentSizeDidChangeForChildContentContainer:container];
    if (container == self.presentedViewController) [self.containerView setNeedsLayout];
}

- (void)containerViewWillLayoutSubviews {
    [super containerViewWillLayoutSubviews];
    self.dimmingView.frame = self.containerView.bounds;
}
@end

疑问:为什么Demo1里没有按照三个步骤里指定 动画的具体细节实现类,也有动画效果呢?

因为在 弹出控制器时,指定了animated为YES,那么此时如果没有实现UIViewControllerAnimatedTransitioning动画细节类,此时就会使用系统的presentViewController动画效果,即:从下往上弹出,从上往下消失。

六.自定义modal转场动画第二个Demo

此Demo2在Demo1的基础上,实现了 UIViewControllerAnimatedTransitioning协议,实现了对动画效果的定制,UIViewControllerAnimatedTransitioning协议主要实现以下几个方法:

/| ------------------------------第三步内容----------------------------------------------
#pragma mark UIViewControllerAnimatedTransitioning具体动画实现
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
    // 动画时长
    return 0.45 ;
}

// 核心,动画效果的实现
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
    // 1.获取源控制器、目标控制器、动画容器View
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    __unused UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    UIView *containerView = transitionContext.containerView;

    // 2. 获取源控制器、目标控制器 的View,但是注意二者在开始动画,消失动画,身份是不一样的:
    // 也可以直接通过上面获取控制器获取,比如:toViewController.view
    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];

    [containerView addSubview:toView];  //必须添加到动画容器View上。

    // 3.设置动画具体细节,使用[UIView animate...]动画,或者其他方式呈现动画。

    // 4.动画结束时,必须执行下句代码
    [transitionContext completeTransition:YES];    
}

- (void)animationEnded:(BOOL) transitionCompleted {
    // 动画结束...
}

在Demo1里,我们在 UIPresentationController 里写死了目标控制器View的位置大小,从面向对象的角度讲,是不合理的,目标控制器View的大小,它自己最清楚,所在我们在Demo2里,仿照苹果官方Demo,完善了这一个问题。(其实如果在上面UIViewControllerAnimatedTransitioning协议里实现了动画前后的位置和大小,那么在UIPresentationController可以不再重写size和frame相关的几个方法)

七.自定义modal转场动画第三个Demo

此效果的实现思路,灵感是从控制器modalPresentationStyle属性值上来的,当modalPresentationStyle为UIModalPresentationCustom的时候,就presentViewController目标控制器之后,把目标控制器的view设置为透明,就能看到源控制器。

toVC.modalPresentationStyle = UIModalPresentationCustom ; //必须是UIModalPresentationCustom
toVC.view.backgroundColor = [UIColor clearColor];  //必须是clearColor
[self presentViewController:toVC animated:NO completion:nil]; //animated:必须是NO

基于以上对目标控制的设置之后,就可以在目标控制器的 viewWillAppear:方法里自定义动画效果。核心代码如下:(建议下载Demo查看详细注释)

#pragma mark - 在此方法做动画呈现
- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.bgView.frame = [UIScreen mainScreen].bounds;
    self.contentView.frame = CGRectMake(0,[UIScreen mainScreen].bounds.size.height, CGRectGetWidth([UIScreen mainScreen].bounds), 300.0);

    [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.95 initialSpringVelocity:0.05 options:UIViewAnimationOptionCurveEaseInOut animations:^{

        self.bgView.alpha = 1.0f ;
        self.contentView.frame = CGRectMake(0, [UIScreen mainScreen].bounds.size.height - 300.f, CGRectGetWidth([UIScreen mainScreen].bounds), 300.0f);

    } completion:^(BOOL finished) {
    }];
}

#pragma mark - 消失
- (void)clickBgView {
    [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.95 initialSpringVelocity:0.05 options:UIViewAnimationOptionCurveEaseInOut animations:^{

        self.bgView.alpha = 0.0f ;
        self.contentView.frame = CGRectMake(0,[UIScreen mainScreen].bounds.size.height, CGRectGetWidth([UIScreen mainScreen].bounds), 300.0);

    } completion:^(BOOL finished) {
        // 动画Animated必须是NO,不然消失之后,会有0.35s时间,再点击无效
        [self dismissViewControllerAnimated:NO completion:nil];
    }];
}

END。

我是小侯爷。

在魔都艰苦奋斗,白天是上班族,晚上是知识服务工作者。

如果读完觉得有收获的话,记得关注和点赞哦。

非要打赏的话,我也是不会拒绝的。

作者:苦笑男神

链接:https://www.jianshu.com/p/a7335474651c

(1)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/ioszidingyizhuanchangdonghua-uipresentationcontroller.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:8 月 20, 2018 at 07:09 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复