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

1、自定义转场

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

2、我的AlertController

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

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等等。

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 <UIViewControllerTransitioningDelegate> transitioningDelegate NS_AVAILABLE_IOS(7_0);

@end

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

@protocol UIViewControllerTransitioningDelegate <NSObject>

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

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed;

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id <UIViewControllerAnimatedTransitioning>)animator;

- (nullable id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)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 <NSObject>

// 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 <UIViewControllerContextTransitioning>)transitionContext;
// This method can only  be a nop if the transition is interactive and not a percentDriven interactive transition.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)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 <UIViewControllerAnimatedTransitioning>)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 <UIViewControllerAnimatedTransitioning>)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 <UIKit/UIKit.h>

@interface KTCenterAnimationController : NSObject <UIViewControllerAnimatedTransitioning>

@end

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

@implementation KTCenterAnimationController

- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)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<UIViewControllerContextTransitioning>)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(我的理解哈)。

QQ20160814-1.png

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

4、后记

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容