让 UIAlertController 兼容 iOS7

公司项目中用到了 UIAlertController 来实现自定义 actionsheet 文字颜色的需求,而 UIAlertController 只能在 iOS8 及更高版本系统使用,在iOS7下会 crash。老大让我写个组件兼容下 iOS7,于是TBAlertController 诞生了。

下面给出的关于 TBAlertController 的代码片段都不是真实源码,只为说明实现的具体思想。

分析问题

为了多快好省的解决当前的问题,我依然使用系统自带的UIAlertController 和 UIActionSheet 分别兼容 iOS8、9 和 iOS7。并且接口与代码中已经存在的 UIAlertController 接口一致,这样只需要将代码中所有的 “UIAlertController” 和 “UIAlertAction” 改为 “TBAlertController” 和 “TBAlertAction” 即可。这样更符合设计模式中“对扩展开放,对修改关闭”的开放原则。

当然最长远的打算应该是自己写个 AlertController,可以随意定制想要的 style,而不受系统控件的风格限制。

技术重点

UIAlertController 与 UIActionSheet 接口上最大的不同之处就是处理按钮点击事件时前者在 block 中实现,后者以 delegate 回调的形式实现。而且还需要高度模仿 UIAlertController 的接口,使原有代码修改量达到最少。而我若想实现一个组件兼容二者,那就必须将它们“装箱”封装。同理,UIAlertAction 也需要类似的处理。

解决思路

构建 TBAlertAction 替代 UIAlertAction

首先,参照 UIAlertAction 的接口,造一个 TBAlertAction 出来。思想是 iOS8以上直接使用 UIAlertAction 来替代,iOS7 则特殊处理,将重要信息保存下来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface TBAlertAction ()
@property (nullable, nonatomic) NSString *title;
@property (nonatomic) TBAlertActionStyle style;
@property (nonatomic) BOOL enabled;
@property (nullable,nonatomic,strong) void (^handler)(TBAlertAction *);
@end

@implementation TBAlertAction
+ (id)actionWithTitle:(NSString *)title style:(TBAlertActionStyle)style handler:(void (^)(TBAlertAction *))handler {
if (iOS8Later) {
UIAlertActionStyle actionStyle = (NSInteger)style;

return [UIAlertAction actionWithTitle:title style:actionStyle handler:(void (^ __nullable)(UIAlertAction *))handler];
}
else {
TBAlertAction *action = [[TBAlertAction alloc] init];
action.title = title;
action.style = style;
action.handler = handler;
action.enabled = YES;
return action;
}
}
@end

这里的 handler block 很重要,在 iOS7 中使用 UIActionSheet 时是在 delegate 回调方法中处理按钮点击事件的,而处理的事务逻辑此时已经写在 handler 中了,后续只需在 delegate 回调方法中正确的执行对应的 block 就行了。

构建 TBAlertController 属性

TBAlertController 也是采取装箱策略,模仿 UIAlertController 的接口,并添加了一个 adaptiveAlert 替身和actions 数组。

TBAlertController 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
- (instancetype)init
{
self = [super init];
if (self) {
if (iOS8Later) {
_adaptiveAlert = [[UIAlertController alloc] init];
}
else {
_adaptiveAlert = [[UIActionSheet alloc] init];
_actions = [NSMutableArray array];
((UIActionSheet *)_adaptiveAlert).delegate = self;
}
[self addObserver:self forKeyPath:@"view.tintColor" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}
return self;
}

- (void)dealloc {
[self removeObserver:self forKeyPath:@"view.tintColor"];
}

+ (instancetype)alertControllerWithTitle:(NSString *)title message:(NSString *)message preferredStyle:(TBAlertControllerStyle)preferredStyle {
TBAlertController *controller = [[TBAlertController alloc] init];
if (iOS8Later) {
controller.adaptiveAlert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:(NSInteger)preferredStyle];
}
else {
switch (preferredStyle) {
case TBAlertControllerStyleActionSheet: {
controller.adaptiveAlert = [[UIActionSheet alloc] initWithTitle:title delegate:controller cancelButtonTitle:nil destructiveButtonTitle:nil otherButtonTitles:nil];
break;
}
case TBAlertControllerStyleAlert: {
controller.adaptiveAlert = [[UIAlertView alloc] initWithTitle:title message:message delegate:controller cancelButtonTitle:nil otherButtonTitles: nil];
break;
}
default: {
break;
}
}
}
return controller;
}

这段实例化 TBAlertController 的方法很好理解,总之就是针对不同情况将 adaptiveAlert 赋予不同的实例。还顺带用 KVO 监听了下 tintColor,这是为了实现当初使用 UIAlertController 的目的-改变字体颜色。

构建 TBAlertController 方法

addAction: 方法的实现类似,也是针对不同情况向 actions 数组添加不同内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
- (void)addAction:(TBAlertAction *)action {
if (iOS8Later) {
[self.adaptiveAlert addAction:(UIAlertAction *)action];
}
else {
[self.actions addObject:action];
NSInteger buttonIndex = [self.adaptiveAlert addButtonWithTitle:action.title];
UIColor *textColor;
switch (action.style) {
case TBAlertActionStyleDefault: {
textColor = self.tintColor;
break;
}
case TBAlertActionStyleCancel: {
[self.adaptiveAlert setCancelButtonIndex:buttonIndex];
textColor = self.tintColor;
break;
}
case TBAlertActionStyleDestructive: {
[self.adaptiveAlert setDestructiveButtonIndex:buttonIndex];
textColor = [UIColor redColor];
break;
}
default: {
textColor = self.tintColor;
break;
}
}
// [((UIButton *)((UIView *)self.adaptiveAlert).subviews.lastObject) setTitleColor:textColor forState:0xFFFFFFFF];
}
}

需要注意的是针对不同 style 的按钮要设置好对应的 buttonindex 和 titleColor。因为苹果可能会拒绝修改系统控件样式的 app 上架,所以我将那行设置颜色的代码注释掉了。

然后在 delegate 中取到对应的 block 并执行:

1
2
3
4
5
6
7
8
#pragma - UIAlertViewDelegate

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
__weak __typeof(self)weakSelf = self;
if (self.actions[buttonIndex].handler) {
self.actions[buttonIndex].handler(weakSelf.adaptiveAlert);
}
}

hook presentViewController: 方法

最后封装下 presentViewController: 就可以了,因为要做到接口与 UIAlertController 一模一样,减少已有代码修改量,需要 hook 到系统的 presentViewController: 方法,并折腾一番:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class aClass = [self class];

SEL originalSelector = @selector(presentViewController:animated:completion:);
SEL swizzledSelector = @selector(tb_presentViewController:animated:completion:);

Method originalMethod = class_getInstanceMethod(aClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(aClass, swizzledSelector);

BOOL didAddMethod =
class_addMethod(aClass,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));


if (didAddMethod) {
class_replaceMethod(aClass,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));

} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});

}

#pragma mark - Method Swizzling

- (void)tb_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
if ([viewControllerToPresent isKindOfClass:[TBAlertController class]]) {
TBAlertController* controller = (TBAlertController *)viewControllerToPresent;
if (iOS8Later) {
((UIAlertController *)controller.adaptiveAlert).view.tintColor = controller.tintColor;
[self tb_presentViewController:((TBAlertController *)viewControllerToPresent).adaptiveAlert animated:flag completion:completion];
}
else {
if ([controller.adaptiveAlert isKindOfClass:[UIAlertView class]]) {
self.tbAlertController = controller;
controller.ownerController = self;
[controller.adaptiveAlert show];
}
else if ([controller.adaptiveAlert isKindOfClass:[UIActionSheet class]]) {
self.tbAlertController = controller;
controller.ownerController = self;
[controller.adaptiveAlert showInView:self.view];
}
}
}
else {
[self tb_presentViewController:viewControllerToPresent animated:flag completion:completion];
}
}

在 Objective-C 中,hook 被称为一种叫做“Method Swizzling”的技术,每种动态语言的 Runtime 系统都支持这些特性。在这里,我在 hook 到的方法里先实例化一个 TBAlertController,然后判断系统版本,分别将 UIAlertController 或UIAlertViewUIActionSheet展示出来。

还需要注意的地方

这里需要注意两点:

  1. 使用 UIAlertController 时,必需在添加完所有 Action 之后才能设定它的 view.tintColor 属性,否则会在 iOS8 下出现问题:取消按钮与其他按钮连成一片。而在 iOS9 下面则不会出现此问题。这也是为什么我会在 hook 到presentViewController: 时才设定它的 tintColor
  2. 一旦 adaptiveAlert 替身被展现在屏幕上, TBAlertController 这个箱子就可能会被释放掉。因为很可能其他人创建 TBAlertController 实例的时候只是个局部变量,一旦出了作用域,它就会被释放掉,而一旦它被提前释放,delegate 回调方法就永远不会执行,前面的努力都白费了,正如下面这样:
    1
    2
    3
    4
    5
    6
    7
    8
    TBAlertController *operationAlertController = [TBAlertController alertControllerWithTitle:@"是否取消关注" message:nil preferredStyle:TBAlertControllerStyleActionSheet];
    [operationAlertController addAction:[TBAlertAction actionWithTitle:@"是" style:TBAlertActionStyleDestructive handler:^(TBAlertAction * _Nonnull action) {
    [self alertControllerHandler:1 clickedButtonAtIndex:0];
    }]];
    [operationAlertController addAction:[TBAlertAction actionWithTitle:@"否" style:TBAlertActionStyleCancel handler:nil]];
    operationAlertController.view.tintColor = [UIColor blackColor];

    [[[TBRootViewController sharedInstance] getCurrentNavigationController] presentViewController:operationAlertController animated:YES completion:nil];

我总不能强制要求所有使用 TBAlertController 的人都要用一个属性来强引用它吧?所以我为 UIViewController 添加了一个类别,目的是为其增加一个属性 tbAlertController(因为 OC 的类别无法为添加的属性自动生成 getter 和 setter,需要使用关联对象动态添加),利用它来保持对“箱子” TBAlertController 的强引用,防止其内存被过早释放。并在 hook 时的 tb_presentViewController: 方法中添加这样一行:

1
self.tbAlertController = controller;

此时又涉及到了另一个问题:内存泄露。因为我们无法确定其他人在实例化 TBAlertAction 时传入的 block 中做了什么,因为它很有可能捕获到了 self!而此时 self 很可能强引用了一个 UIViewController,然后其tbAlertController 属性又强引用了 TBAlertController,这个 TBAlertController 的 actions 数组中的一个TBAlertAction 强引用了这个 block。好长的一个保留环啊!那么如何打破这个环呢?我总不能要求使用者必需在 block 内外做个 Weak/Strong Dance 吧!毕竟“谁创建,谁释放”的规则我们还是要遵守的,必需在组件内部解决可能发生的内存泄露问题。于是我给 TBAlertController 又添加了一个属性 ownerController,注意内存管理语义是 weak

1
@property (nullable,nonatomic,weak) UIViewController *ownerController;

然后在 tb_presentViewController: 方法中再添加一行代码,将 TBAlertController 的 ownerController 设为调用 presentViewController: 方法的 controller:

1
controller.ownerController = self;

最后在 UIAlertView 或 UIActionSheet 消失时将 tbAlertController 设为 nil 就打破保留环了,它原本指向TBAlertController 自己,设为 nil 后,没有对象引用 TBAlertController 实例了,其引用计数为零,然后被释放:

1
2
3
- (void)actionSheet:(UIActionSheet *)actionSheet didDismissWithButtonIndex:(NSInteger)buttonIndex {
self.ownerController.tbAlertController = nil;
}

除此之外还有很多细节没有在这里阐述,比如对属性的封装,还有对 addTextFieldWithConfigurationHandler: 等接口的封装等。

其实早已有人做过类似的事情,将系统组件封装成兼容的版本:PSTAlertController。但其接口与原生的UIAlertController 差很多,需要手动替换很多已有的代码。

你可能感兴趣的:(让 UIAlertController 兼容 iOS7)