让 UIAlertController 兼容 iOS7
来源:互联网 发布:淘宝三个评价 编辑:程序博客网 时间:2024/05/17 01:12
公司项目中用到了 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
数组。
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
或UIAlertView
、UIActionSheet
展示出来。
还需要注意的地方
这里需要注意两点:
- 使用
UIAlertController
时,必需在添加完所有 Action 之后才能设定它的view.tintColor
属性,否则会在 iOS8 下出现问题:取消按钮与其他按钮连成一片。而在 iOS9 下面则不会出现此问题。这也是为什么我会在 hook 到presentViewController:
时才设定它的tintColor
。 - 一旦
adaptiveAlert
替身被展现在屏幕上,TBAlertController
这个箱子就可能会被释放掉。因为很可能其他人创建TBAlertController
实例的时候只是个局部变量,一旦出了作用域,它就会被释放掉,而一旦它被提前释放,delegate 回调方法就永远不会执行,前面的努力都白费了,正如下面这样:1
2
3
4
5
6
7
8TBAlertController *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
- 让UIAlertController同时兼容iphone和ipad
- ios7兼容
- iOS兼容开发:让程序同时支持iOS6和iOS7
- iOS7以上系统自定义UIAlertController
- 兼容IOS7和IOS6
- ios7.04 兼容插件
- ios6 ios7 同时兼容
- iOS7 兼容适配
- iOS7 兼容适配
- ios7 兼容之前版本
- iOS6兼容iOS7界面
- ios7 兼容之前版本
- ios7 兼容之前版本
- iOS推送兼容iOS7
- 兼容iOs7的自定义alertView
- 项目对IOS7的兼容
- 兼容iOs7的自定义alertView
- 使用JSONP解决Ajax跨域访问问题
- Android SDK Manager无法更新列表
- UIView如何管理它的子视图
- HttpResponseMessage && IHttpActionResult
- 公用分页模块之knockout
- 让 UIAlertController 兼容 iOS7
- TF400976: SQL Server 服务帐户 NT Service\MSSQLSERVER 没有在备份路径 \\XXX\XX 创建备份的必要权限
- Lowest Common Ancestor of a Binary Tree | Java最短代码实现
- 同步或者重构Activiti Identify用户数据的多种方案比较
- 图片处理
- Hadoop的namenode datanode无法启动
- FastDFSClient.java工具方法
- 第二堂课
- 获取指定包下的所有类