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

2019-03-29  本文已影响0人  boy丿log

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地址

上一篇下一篇

猜你喜欢

热点阅读