链式语法封装一个简单的UIAlertController

UIAlertView在iOS9后就不建议被使用,官方要求最好使用UIAlertViewController,它集成了UIAlertView和UIActionSheet。UIAlertView和UIActionSheet是建立在View的基础上,调用show方法可以直接显示,并通过代理来实现点击方法的回调,而UIAlertController是建立在UIViewController之上的,需要present显示,并且所有的点击按钮都通过UIAction的model进行创建,当然两者还有很多区别的。今天,我们关注的重点是在项目中有大量使用的UIAlertView和UIActionSheet如何被替换为UIAlertViewController,如果按正常的逻辑:

UIAlertController创建和显示

而UIAlertView要先遵循代理:

UIAlertView创建和显示

可以看出,如果在项目中有需要将UIAlertView中替换成UIAlertController还是需要做很多工作的。今天,我们来简化一下这个工作,需要写一个UIAlertController的分类,效果是这样:


封装后的UIAlertViewController

可以看到,封装后的可以很方便的兼容UIAlertView的代理方法,而且只需要一行代码。

接下来看封装的头文件

/**
 快速创建
 */
static inline UIAlertController *UIAlertControllerCreate( NSString *_Nullable title, NSString *_Nullable message, UIAlertControllerStyle style){
    return [UIAlertController alertControllerWithTitle:title message:message preferredStyle:style];
}
static inline UIAlertController *UIAlertControllerAlertCreate(NSString *_Nullable title,NSString *_Nullable message){
    return [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
}
static inline UIAlertController *UIAlertControllerSheetCreate(NSString *_Nullable title, NSString *_Nullable message){
    return [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleActionSheet];
}

typedef void (^ wtcAlertTapBlock)(NSInteger index, UIAlertAction *action);
@interface UIAlertController (WTCCategory)

/**
 添加Action,并设置key值,需要在点击方法中使用
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ addAction)(NSString *title, UIAlertActionStyle style, NSInteger index);
@property (nonatomic, copy, readonly) UIAlertController * (^ addDesAction)(NSString *title, NSInteger index);
@property (nonatomic, copy, readonly) UIAlertController * (^ addCancelAction)(NSString *title, NSInteger index);
@property (nonatomic, copy, readonly) UIAlertController * (^ addDefaultAction)(NSString *title, NSInteger index);


@property (nonatomic, copy, readonly) UIAlertController * (^ addTextField) (void (^ textField) (UITextField *textField));
/**
 在点语法中用来返回一个最近添加的UIAlertAction,用来设置样式
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ actionStyle) (void (^ actionStyle)(UIAlertAction * action));


/**
 action点击方法,返回的key值是上面添加的key值
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ actionTap) (wtcAlertTapBlock tapIndex);


/**
 在点语法中用来返回当前的UIAlertVController,用来设置样式
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ alertStyle) (void (^ alert)(UIAlertController *alertVC));


/**
 title样式设置
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ alertTitleAttributeFontWithColor)(UIFont *font, UIColor *color);
@property (nonatomic, copy, readonly) UIAlertController * (^ alertTitleAttributeWidthDictionary)(void (^attribute)(NSMutableDictionary * attributes));

/**
 message样式设置
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ alertMessageAttributeFontWithColor)(UIFont *font, UIColor *color);
@property (nonatomic, copy, readonly) UIAlertController * (^ alertMessageAttributeWidthDictionary)(void (^attribute)(NSMutableDictionary * attributes));
//detail
//@property (nonatomic, copy, readonly) UIAlertController * (^ alertDetailAttributeFontWithColor)(UIFont *font, UIColor *color);
//@property (nonatomic, copy, readonly) UIAlertController * (^ alertDetailAttributeWidthDictionary)(void (^attribute)(NSMutableDictionary * attributes));


/**
 title属性
 */
@property (nonatomic, copy, readonly) UIAlertController * (^ alertTitleMaxNum)(NSUInteger numberOfLines);
@property (nonatomic, copy, readonly) UIAlertController * (^ alertTitleLineBreakMode)(NSLineBreakMode mode);
/**
 设置title字体颜色
 */
- (void)setACTitleAttributedString:(nullable NSAttributedString *)attributedString;

/**
 设置message字体颜色
 */
- (void)setACMessageAttributedString:(nullable NSAttributedString *)attributedString;

/**
 设置介绍字体颜色
 */
- (void)setACDetailAttributedString:(nullable NSAttributedString *)attributedString;

/**
 设置title最大行数
 */
- (void)setACTitleLineMaxNumber:(NSInteger)number;

/**
 设置title截断模式
 */
- (void)setACTitleLineBreakModel:(NSLineBreakMode)mode;

/**
 添加action
 */
- (UIAlertAction *)addActionTitle:(NSString *)title style:(UIAlertActionStyle)style block:(void (^) (UIAlertAction *action))block;


@end

刚上来的3个函数方法用来便捷构造,接下来是一些readonly的block属性,是为了用来实现链式语法(在我们平常使用block是用来回调的,链式语法使用相反的思想,本类回调本类block并返回本类,借助block实现了链式语法)。还有一些通过kvo设置UIAlertController属性的一些方法。

在实现的时候需要解决几个问题:

1.如何统一action点击事件,并区分。
2.如何在action点击时直接获取当前AlertController
3.如何确定当前的presentingViewController

问题一:如何统一action点击事件,并区分。

解决方法如下:

- (UIAlertController * _Nonnull (^)(NSString * _Nonnull, UIAlertActionStyle, NSInteger))addAction{
    return ^ (NSString *title, UIAlertActionStyle style, NSInteger index){
        
        __weak typeof(self)weakSelf = self;
        [self addActionTitle:title style:style block:^(UIAlertAction * _Nonnull action) {
            if ([weakSelf wtc_actionBlock]) {
                [weakSelf wtc_actionBlock](index, action);
            }
        }];
        return self;
    };
}

block拥有的特性是可以保存变量,所以在执行addAction这个操作时可以给action添加一个数值类型的值,在执行点击block回调点击的actionTap方法时回调这个值用来判断。

typedef void (^ wtcAlertTapBlock)(NSInteger index, UIAlertAction *action);
- (UIAlertController * _Nonnull (^)(wtcAlertTapBlock _Nonnull))actionTap{
    return ^ (wtcAlertTapBlock block){
        [self setWtc_actionBlock:block];
        return self;
    };
}
- (wtcAlertTapBlock)wtc_actionBlock{
    return objc_getAssociatedObject(self, kwtcActionBlock);
}

- (void)setWtc_actionBlock:(wtcAlertTapBlock)block{
    objc_setAssociatedObject(self, kwtcActionBlock, block,OBJC_ASSOCIATION_COPY);
}

关于点击的实现,可以定义一个block,返回action和设置时传入的标识(Index)来进行action的区分。用runtime动态绑定一个wtcAlertTapBlock类型的block,并在执行actionTap设置这个block。

- (UIAlertController * _Nonnull (^)(void (^ _Nonnull)(UIAlertAction * _Nonnull)))actionStyle{
    return ^ (void (^style) (UIAlertAction *action)){
        if (style) {
            style([self wtc_currentAction]);
        }
        return self;
    };
}
- (UIAlertAction *)wtc_currentAction{
    return [self.actions lastObject];
}

通过UIAlertController的actions属性(能获取当前添加所有的action)来获取最新添加的action,并设置样式。

经过一系列的操作,我们就能够方便的设置action和处理回调了。

问题二:如何在action点击时直接获取当前AlertController

解决方法:
在分类中添加属性alertViewController

@interface UIAlertAction (WTCCategory)

@property (nonatomic, weak, readonly) UIAlertController * alertViewController;
/**
 设置action颜色
 */
- (void)setAlertActionTitleColor:(UIColor *)color;

/**
 设置action图片
 
 */
- (void)setAlertImage:(UIImage *)image;


@end

我们都知道分类不可以添加属性,只能通过runtime来实现,但是因为UIAlertController是持有action的,所以如果action强持有AlertViewController会造成循环引用,所以这里属性只能用weak,然而,

runtime绑定属性策略

可以看出是没有weak这个属性的,解决这个问题有两个思路,
第一,用:OBJC_ASSOCIATION_ASSIGN,在UIAlertController的dealloc方法中将action中的alertViewController置nil,避免野指针,但是这样还需要用到runtime的方法交换,太麻烦了。
第二:设置一个中间者weak持有UIAlertController,action强持有中间者,这里我们就用的这种方法。

@interface UIAlertActionWithController : NSObject
@property (nonatomic, weak) UIAlertController * alertViewController;
@end
@implementation UIAlertActionWithController


@end

@implementation UIAlertAction (WTCCategory)

- (void)setAlertViewController:(UIAlertActionWithController *)model{
    objc_setAssociatedObject(self, kWTCCategoryActionViewController, model, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIAlertActionWithController *)alertViewActionWithController{
    return objc_getAssociatedObject(self, kWTCCategoryActionViewController);
}

- (UIAlertController *)alertViewController{
    return [self alertViewActionWithController].alertViewController;
}

- (void)setAlertActionTitleColor:(UIColor *)color{
    [self setValue:color forKey:@"_titleTextColor"];
}

- (void)setAlertImage:(UIImage *)image{
    [self setValue:[image imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal] forKey:@"image"];
}

@end

在添加Action的时候

- (UIAlertAction *)addActionTitle:(NSString *)title style:(UIAlertActionStyle)style block:(void (^)(UIAlertAction * _Nonnull))block{
    UIAlertAction *action = [UIAlertAction actionWithTitle:title style:style handler:block];
    [self addAction:action];
    UIAlertActionWithController *model = [UIAlertActionWithController new];
    model.alertViewController = self;
    [action setAlertViewController:model];
    return action;
}

这样我们就实现了,从action中获取action的AlertViewController。

问题三:如何确定当前的presentingViewController

/**
 立刻显示
 */
- (void)showInRootViewController;
//显示延时time小时
- (void)showAndDissmissAfterTime:(NSTimeInterval)time;

如果在某个model、view或者非ViewController中我们也想像UIAlertView一样方便,设置完成后只调用一个show方法就可以弹框该怎么办呢,那就只能从AppDelegate中获取当前显示的控制器了。

- (void)showInRootViewController{
    UIViewController *vc = [UIApplication currentTopViewController];
    [self presentedFromViewController:vc];
}

- (void)showAndDissmissAfterTime:(NSTimeInterval)time{
    if (time > 0) {
        UIViewController *vc = [UIApplication currentTopViewController];
        [vc presentViewController:self animated:YES completion:^{
            dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
                [self dismissViewControllerAnimated:NO completion:nil];
            });
        }];
    }
}

UIApplication分类方法

+ (UIViewController *)currentTopViewController{
    UIViewController *vc = [self rootViewController];
    Class naVi = [UINavigationController class];
    Class tabbarClass = [UITabBarController class];
    BOOL isNavClass = [vc isKindOfClass:naVi];
    BOOL isTabbarClass;
    if (!isNavClass) {
        isTabbarClass = [vc isKindOfClass:tabbarClass];
    }
    while (isNavClass || isTabbarClass) {
        UIViewController * top;
        if (isNavClass) {
          top = [(UINavigationController *)vc topViewController];
        }else{
          top = [(UITabBarController *)vc selectedViewController];
        }
        if (top) {
            vc = top;
        }else{
            break;
        }
        isNavClass = [vc isKindOfClass:naVi];
        if (!isNavClass) {
            isTabbarClass = [vc isKindOfClass:tabbarClass];
        }
    }
    return vc;
}
+ (UIWindow *)delegateWindow{
    return [UIApplication sharedApplication].delegate.window;
}
+ (id)rootViewController{
    return [self delegateWindow].rootViewController;
}

此处需要注意的是:

Class naVi = [UINavigationController class];
    Class tabbarClass = [UITabBarController class];
    BOOL isNavClass = [vc isKindOfClass:naVi];
    BOOL isTabbarClass;
    if (!isNavClass) {
        isTabbarClass = [vc isKindOfClass:tabbarClass];
    }
    while (isNavClass || isTabbarClass) {
        UIViewController * top;
        if (isNavClass) {
          top = [(UINavigationController *)vc topViewController];
        }else{
          top = [(UITabBarController *)vc selectedViewController];
        }
        if (top) {
            vc = top;
        }else{
            break;
        }
        isNavClass = [vc isKindOfClass:naVi];
        if (!isNavClass) {
            isTabbarClass = [vc isKindOfClass:tabbarClass];
        }
    }

这个递归,vc现获取到跟控制器,然后判断是否为NavgationController或TabbarController,如果是,则继续,否则,返回这个控制器。

总结

经过上面的一系列封装,我们就可以很方便的将UIAlertController封装的和UIAlertView一样简单,并添加更方便的功能。

github地址

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