公司项目中用到了 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; } }
} }
|
需要注意的是针对不同 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
或UIAlertView
、UIActionSheet
展示出来。
还需要注意的地方
这里需要注意两点:
- 使用
UIAlertController
时,必需在添加完所有 Action 之后才能设定它的 view.tintColor
属性,否则会在 iOS8 下出现问题:取消按钮与其他按钮连成一片。而在 iOS9 下面则不会出现此问题。这也是为什么我会在 hook 到presentViewController:
时才设定它的 tintColor
。
- 一旦
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
差很多,需要手动替换很多已有的代码。