使用自定义转场模仿UIAlertController搭建自己的弹出框

1、自定义转场

关于iOS 7出来的view controller转场api,网上有很多文章了,推荐去看这个iOS视图控制器转场详解,这里就不仔细介绍了,后面会讲一点用到的基本概念和内容。

2、我的AlertController

这是我的AlertController,相比系统自带的,多了几种动画,还有可以自定义字体,样式等等:

使用自定义转场模仿UIAlertController搭建自己的弹出框_第1张图片
alertController.gif

3、实现

1、看看系统的UIAlertController是怎么用的:

    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"这是一个alert" message:@"又如何?" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
    [alert addAction:cancel];
    [self presentViewController:alert animated:YES completion:nil];

UIAlertController是继承UIViewController的,那么我们自定义一个KTAlertController同样继承UIViewController,然后presentViewController转场的时候展示KTAlertController就行了。我直接用的xib搭建KTAlertController,直观一点,也好利用自动布局。背景view设置为black alpha为0.3,因为展示UIViewController背景要透明黑。然后中间放contentView,设置圆角,各种子view,button等等。

使用自定义转场模仿UIAlertController搭建自己的弹出框_第2张图片
QQ20160814-0.png

这里考虑到title和description有可能没有或者多行的情况,也考虑到底部的button只有一个的情况,使用自动布局进行适配,整个alert view的大小会根据内容多少进行适配,具体的设置查看 我的工程即可。

2、实际上你在调用[self presentViewController:alert animated:YES completion:nil]的时候,会发现系统已经给你做好了一个动画,是从底部往上弹,还有就是原来的view controller(也就是[self presentViewController:alert animated:YES completion:nil]中的self)不见了,我们要的不是这样的效果。这里presentViewController使用的是Modal转场,UIKit 已经为 Modal 转场实现了多种效果,当 UIViewController的modalPresentationStyle属性为UIModalPresentationCustom或UIModalPresentationFullScreen时,我们就有机会定制转场效果,此时modalTransitionStyle指定的转场动画将会被忽略。因此我们需要改写modalPresentationStyle的默认属性值,默认是UIModalTransitionStyleCoverVertical,也就是从下往上,原来的view controller不见了的形式。我们改为custom形式:

+ (instancetype)alertControllerWithTitle:(NSString *)title description:(NSString *)description cancel:(NSString *)cancel button:(NSString *)button action:(void (^)())buttonAction
{
    NSAssert(title.length > 0 || description.length > 0 , @"title和description不能同时为空");
    
    KTAlertController *alert = [[KTAlertController alloc] init];
    alert.modalPresentationStyle = UIModalPresentationCustom;
    alert.titleText = title;
    alert.descriptionText = description;
    alert.cancelText = cancel ? cancel : @"取消";
    alert.buttonText = button;
    alert.buttonAction = buttonAction;
    
    return alert;
}

这样可以自定义效果,同时原来的view controller的view并不会从视图结构中删除。

3、转场动画控制器和转场代理
在自定义转场动画的时候需要提供给view controller转场代理,然后由转场代理在合适的时候提供给view controller转场动画控制器,由转场控制器告诉view controller你得怎么转场。比如拿我们熟悉的tableView来说,你会给tableView一个dataSource代理,然后tableView会调用代理方法tableView:cellForRowAtIndexPath:来问代理,我的每一行显示什么cell啊?这里是一样的,view controller也会问transitioningDelegate:
viewController:老兄,我马上要被present了,好激动啊,我该怎么出场闪瞎他们的狗眼呢?
transitioningDelegate:,好吧,我给你一个动画控制器吧,它里面有两个方法,会告诉你该怎么出场的。。。
viewController:哇,太好了,快给我吧!
这里transitioningDelegate就是用来提供转场动画控制器的,真正负责转场动画的,当然是转场动画控制器了。transitioningDelegate是普通的viewController都有的属性,定义是:

@interface UIViewController(UIViewControllerTransitioning)

@property (nullable, nonatomic, weak) id  transitioningDelegate NS_AVAILABLE_IOS(7_0);

@end

这里遵守UIViewControllerTransitioningDelegate协议,对于UINavigationController还有遵守UINavigationControllerDelegate的属性delegate,对于UITabBarController还有遵守UITabBarControllerDelegate的属性delegate。
看看UIViewControllerTransitioningDelegate的定义:

@protocol UIViewControllerTransitioningDelegate 

@optional
- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source;

- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed;

- (nullable id )interactionControllerForPresentation:(id )animator;

- (nullable id )interactionControllerForDismissal:(id )animator;

- (nullable UIPresentationController *)presentationControllerForPresentedViewController:(UIViewController *)presented presentingViewController:(UIViewController *)presenting sourceViewController:(UIViewController *)source NS_AVAILABLE_IOS(8_0);

@end

很显然第一个方法和第二个方法是分别在present和dismiss的时候提供动画控制器animationController的,这里的动画控制器必须遵守UIViewControllerAnimatedTransitioning协议,后面的三个方法是交互控制和返回presentationController的,暂时不用关心。
看来动画控制器不是随便一个object,必须遵守一定的规则才能让viewController知道如何动画,下面就是规则:

@protocol UIViewControllerAnimatedTransitioning 

// This is used for percent driven interactive transitions, as well as for container controllers that have companion animations that might need to
// synchronize with the main animation. 
- (NSTimeInterval)transitionDuration:(nullable id )transitionContext;
// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id )transitionContext;

@optional

// This is a convenience and if implemented will be invoked by the system when the transition context's completeTransition: method is invoked.
- (void)animationEnded:(BOOL) transitionCompleted;

@end

作为动画控制器,必须实现前面两个方法,第一个方法是告诉动画持续时间,第二个方法是告诉具体怎样动画。有了这两个,viewController就知道怎么动画了。

4、那么在构造器里面指定transitioningDelegate,这里指定自己为代理,modalPresentationStyle改为UIModalPresentationCustom

+ (instancetype)alertControllerWithTitle:(NSString *)title description:(NSString *)description cancel:(NSString *)cancel button:(NSString *)button action:(void (^)())buttonAction
{
    NSAssert(title.length > 0 || description.length > 0 , @"title和description不能同时为空");
    
    KTAlertController *alert = [[KTAlertController alloc] init];
    alert.transitioningDelegate = alert;
    alert.modalPresentationStyle = UIModalPresentationCustom;
    alert.titleText = title;
    alert.descriptionText = description;
    alert.cancelText = cancel ? cancel : @"取消";
    alert.buttonText = button;
    alert.buttonAction = buttonAction;
    
    return alert;
}

实现代理方法:

- (nullable id )animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    switch (self.animationType) {
        case KTAlertControllerAnimationTypeCenterShow:
            return [[KTCenterAnimationController alloc] init];
            break;
            
        case KTAlertControllerAnimationTypeUpDown:
            return [[KTUpDownAnimationController alloc] init];
            break;
            
        default:
            break;
    }
}

- (nullable id )animationControllerForDismissedController:(UIViewController *)dismissed
{
    switch (self.animationType) {
        case KTAlertControllerAnimationTypeCenterShow:
            return [[KTCenterAnimationController alloc] init];
            break;
            
        case KTAlertControllerAnimationTypeUpDown:
            return [[KTUpDownAnimationController alloc] init];
            break;
            
        default:
            break;
    }
}

这里present和dismiss都用同一个动画控制器,当然可以分开写两个动画控制器分别控制present和dismiss,但是没必要哈,因为很简单。根据animationType(自定义的枚举属性)来返回不同的动画控制器实例,以此达到可以使用多种动画效果的目的。比如KTCenterAnimationController的实现:

// 头文件
#import 

@interface KTCenterAnimationController : NSObject 

@end

// 实现文件
#import "KTCenterAnimationController.h"
#import "KTAlertController.h"

@implementation KTCenterAnimationController

- (NSTimeInterval)transitionDuration:(id)transitionContext
{
    // 1
    UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    if (toVC.isBeingPresented) {
        return 0.3;
    }
    else if (fromVC.isBeingDismissed) {
        return 0.1;
    }
    
    return 0.3;
}

- (void)animateTransition:(id)transitionContext
{
    KTAlertController *toVC = (KTAlertController *)[transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    if (!toVC || !fromVC) {
        return;
    }
    UIView *containerView = [transitionContext containerView];
    NSTimeInterval duration = [self transitionDuration:transitionContext];
    if (toVC.isBeingPresented) {
        // 2
        [containerView addSubview:toVC.view];
        toVC.view.frame = CGRectMake(0.0, 0.0, containerView.frame.size.width, containerView.frame.size.height);
        toVC.backView.alpha = 0.0;
        CGAffineTransform oldTransform = toVC.contentView.transform;
        toVC.contentView.transform = CGAffineTransformScale(oldTransform, 0.3, 0.3);
        toVC.contentView.center = containerView.center;
        [UIView animateWithDuration:duration animations:^{
            toVC.backView.alpha = 0.3;
            toVC.contentView.transform = oldTransform;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
    else if (fromVC.isBeingDismissed) {
        // 3
        [UIView animateWithDuration:duration animations:^{
            fromVC.view.alpha = 0.0;
        } completion:^(BOOL finished) {
            [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
        }];
    }
}

@end

1处返回动画时间根据present和dismiss区别,毕竟dismiss大家都喜欢它快一点滚是吧,这里在present和dismiss的时候,fromVC和toVC是不同的,在present的时候,我们的KTAlertController显然是toVC,但是dismiss的时候,它就变成了fromVC了,因为你是要从这个KTAlertController切换到底下的那个viewController。transitionContext这个参数可以告诉我们很多信息,fromVC, toVC,containerView等, containerView是装载要被present的KTAlertController的,这是系统提供的。进入视图调试,当一个KTAlertController被present之后,你可以看到一个透明的view,处于底下那个viewController的view和KTAlertController的view之间,就应该是containerView(我的理解哈)。

使用自定义转场模仿UIAlertController搭建自己的弹出框_第3张图片
QQ20160814-1.png

2处在present的时候,注意要将toVC的view添加到containerView,然后就是设置大小位置,动画等。
3处注意不需要添加fromVC的view到containerView上面,因为fromVC本来就是要消失的,toVC的view就更不能添加了,因为toVC此时就是底下的那个viewController。

4、后记

完整项目在这里,后续有时间再完善,还可以添加几种动画效果的!

你可能感兴趣的:(使用自定义转场模仿UIAlertController搭建自己的弹出框)