在开发中经常遇到一些需要弹框 UIAlertController 一直显示不消失的需求,如登录时等。但一般的,不管点击取消或确定按钮,弹框都会消失。今天就用 Runtime 这个“黑魔法”来实现这个需求。

首先在 UIAlertAction 的回调里打个断点,看下 UIAlertController 调用的方法:

Screenshot

可以看到 UIAlertController 调了 _invokeHandlersForAction:_dismissAnimated:triggeringAction:triggeredByPopoverDimmingView:dismissCompletion: 两个方法,可以猜测前一个方法是用来回调 UIAlertAction 的,后一个方法可能与 UIAlertController 的消失与否有关,可以来验证一下。

UIAlertController+Dismiss.h

1
2
3
4
5
6
7
8
//  UIAlertController+Dismiss.h
#import <UIKit/UIKit.h>

@interface UIAlertController (Dismiss)

@property (nonatomic, assign) BOOL wy_rejectDismiss;

@end

UIAlertController+Dismiss.m

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//  UIAlertController+Dismiss.m
#import "UIAlertController+Dismiss.h"
#import <objc/runtime.h>

@implementation UIAlertController (Dismiss)

@dynamic wy_rejectDismiss;

- (void)setWy_rejectDismiss:(BOOL)wy_rejectDismiss {
objc_setAssociatedObject(self, @selector(wy_rejectDismiss), @(wy_rejectDismiss), OBJC_ASSOCIATION_ASSIGN);
}

- (BOOL)wy_rejectDismiss {
return [(NSNumber *)objc_getAssociatedObject(self, _cmd) boolValue];
}

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];

SEL originalSelector = NSSelectorFromString(@"_dismissAnimated:triggeringAction:triggeredByPopoverDimmingView:dismissCompletion:");
SEL swizzledSelector = @selector(wy_dismissAnimated:
triggeringAction:
triggeredByPopoverDimmingView:
dismissCompletion:);

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

// 动态添加方法,如果类中不存在这个方法的实现,则添加成功
// 这里 UIAlertController 类中存在 originalMethod,所以添加是失败的
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
// 如果添加成功,则用 originalMethod 替换添加的空方法 originalMethod
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
// 交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

- (void)wy_dismissAnimated:(BOOL)animation
triggeringAction:(UIAlertAction *)action
triggeredByPopoverDimmingView:(id)view
dismissCompletion:(id)handler {
// 如果点击“取消”按钮或者允许弹框 dismiss,就调用原来的方法(originalMethod)
// 因为已经交换了两个方法的实现,所以其实是调用 swizzledMethod
// 所以这里并不会出现循环调用
// 否则就忽略原来的方法(originalMethod),直接下一步,掉用后面的方法
if (action.style == UIAlertActionStyleCancel || self.wy_rejectDismiss == NO) {
[self wy_dismissAnimated:animation
triggeringAction:action
triggeredByPopoverDimmingView:view
dismissCompletion:handler];
} else {
SEL invokeHandler = NSSelectorFromString(@"_invokeHandlersForAction:");
// 这里如果使用 performSelector 来调 invokeHandler 这个方法
// [self performSelector:invokeHandler withObject:action];
// 会报 "PerformSelector may cause a leak because its selector is unknown" 的警告
// 为消除警告,用下面的方法
IMP imp = [self methodForSelector:invokeHandler];
void (*func)(id, SEL, UIAlertAction *) = (void *)imp;
func(self, invokeHandler, action);
}
}

@end

ViewController.m

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
//  ViewController.m
#import "ViewController.h"
#import "UIAlertController+Dismiss.h"

@interface ViewController ()

@property (nonatomic, strong) UIAlertController *alertController;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
}

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
[textField addTarget:self action:@selector(handleTextFieldEditingChanged:) forControlEvents:UIControlEventEditingChanged];
}];
UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
// do something
}];
UIAlertAction *confirmActin = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
// do something
}];
[alertController addAction:cancelAction];
[alertController addAction:confirmActin];
alertController.wy_rejectDismiss = YES;
self.alertController = alertController;
[self presentViewController:alertController animated:YES completion:nil];
}

- (void)handleTextFieldEditingChanged:(UITextField *)textField {
if ([textField.text isEqualToString:@"123"]) {
self.alertController.wy_rejectDismiss = NO;
} else {
self.alertController.wy_rejectDismiss = YES;
}
}

@end

结果说明猜测是正确的。这样就实现了禁止弹框消失的需求,只需要设置 alertController 的 wy_rejectDismiss 这个属性为 YES,当需要弹框消失时只要把 wy_rejectDismiss 设置成 NO 即可。

相关链接

Method Swizzling
Runtime 隐藏 Status Bar 背景
performSelector may cause a leak because its selector is unknown