iOS中设计一个Block代码执行的UIAlertView

http://bluereader.org/article/102764089

Windows下的AlertView(比如java的MessageBox, JS的alert('title'))都是阻塞代码继续执行的,举个例子

int result = window.confirm('你会点击取消按钮么?');
console.log("If I havn't confirm, I won't be logged."); 
if (result == 1) { 
// some code 
}

iOS原生提供的UIAlertView就不能实现类似的效果,但是依旧可以自己Custom实现,本篇博客将介绍如何在iOS下实现类似windows这样的弹出框,如果不点击确认按钮则不执行后续的代码。

先简单介绍一下iOS中的UIAlertView:UIAlertView是以回调的形式通知调用方,调用完show方法之后,后续的代码可以继续执行,等AlertView处理完之后,会异步通知掉用方我刚才进行了什么操作。

在开始这篇文章之前,你需要准备一些知识(NSRunloop),很多博客有专门的介绍,为了避免重复的博客,在这里不做详细的介绍。如果你还没有了解,那么你得先去了解,可以直接Google NSRunloop 或者直接看官方文档 Threading Programming Guide: Run Loops,或者如果你不想了解Runloop,如果你有需求,你可以直接下载最后完成的代码。

准备好了知识之后,我们来考虑一下如何设计我们的AlertView,命名暂定为STAlertView。我们大概要定义一个方法,

- (NSInteger)showInView:(UIView *)view animated:(BOOL)animated;

我们考虑下如何才能让代码不能继续执行,但是我们可以让应用接受所有的用户事件呢?

先补充点知识:iOS的程序运行是事件驱动的,这点本质上和Windows的消息驱动是一样的。意思就是说,iOS维护者一个事件队列,主线程不断的从事件队列中去取事件,然后回调给我们的程序去处理,比如我们按钮的按下效果,点击事件,列表滚动等等,当然事件不止包含点击事件,这里就简单说明下。

再补充一点知识:大家可以在上面给出的苹果文档中看到,Runloop 提供一个run方法,这个方法可以使Runloop 处于某种状态,直到时间到达指定的limitDate,其实这些也就是Runloop能在闲事处于低耗的原因。没有事件,就当前线程处于休眠状态,如果有事件就执行事件,这就是Runloop能够减少CPU空转的原因。

在正式开始之前,我们先来看一个例子,例子很简单,只有一个while(true)循环,简单代码如下:

[TestRunloop]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    UIButton *button1 = [UIButton buttonWithType:UIButtonTypeCustom];
    button1.frame = CGRectMake(100, 100, 100, 30);
    [button1 setTitle:@"测试按钮" forState:UIControlStateNormal];
    [button1 setTitleColor:[UIColor blueColor] forState:UIControlStateNormal];
    [button1 setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
    [button1 addTarget:self action:@selector(testButtonActionFired:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button1];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"Did Start viewDidAppear");
    while (!_shouldContinue) {
        [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    NSLog(@"Will End viewDidAppear");
}

- (void)testButtonActionFired:(UIButton *)button {
    NSLog(@"TestButtonActionFired");
}

代码ViewDidLoad中创建了一个标题为测试按钮的Button,为其设置了按下和正常的文本颜色,添加了一个点击事件。ViewDidAppwar中(_shouldContinue为一个全局BOOL变量,值为false),写了一个while(true)循环,里面的内容就是让Runloop处于DefaultMode

代码的执行效果我们可以先预测一下(我们预测打印的值)

这一句肯定会被执行 NSLog(@"Did Start viewDidAppear");如果没有执行,说明你的这个view没有被添加到window上,

这一句肯定不会被执行 NSLog(@"Will End viewDidAppear"); 由于上面有while(true),相当于死循环,所以之后的内容肯定不被执行到,如果对以上两点有异议的,建议可以先了解一下基础。

那么问题来了 ?

按钮的事件会不会被执行到,按钮的点击效果会不会实现?

在博客前面说过一些关于Runloop的,大家可以拿上面的例子验证一下(触摸事件算一种事件源),处于Default / UITrackingMode的Runloop是可以接受用户触摸事件的。所以答案揭晓,按钮的点击动作可以执行,点击效果可以显示,我们看一下点击三次测试按钮的打印的日志,后续会附带源码下载地址

[打印日志]
1
2
3
4
2014-10-27 15:29:36.796 STRunloop[3474:195616] Did Start viewDidAppear

2014-10-27 15:29:39.493 STRunloop[3474:195616] TestButtonActionFired

2014-10-27 15:29:40.487 STRunloop[3474:195616] TestButtonActionFired

2014-10-27 15:29:40.902 STRunloop[3474:195616] TestButtonActionFired

代码如预期执行,没有错的,这样看来,我们现在需要做的就是保证事件机制的正常运行,以及block住当前代码就可以了,根据上面的知识,我们可以得到一个方法原型

[ShowInView]
1
2
3
4
5
6
7
- (NSInteger)showInView:(UIView *)view animated:(BOOL)animated {
    //
    while (!_shouldContinue) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    return _dismissedIndex;
}

我们只需要在用户点击完完成按钮之后,为_dismissedIndex和_shouldContinue赋值就可以了。到这里之后,我们差不多就已经实现了一个Block执行的AlertView,剩余的代码都不复杂,我就直接贴代码了

[STAlertView.h]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//
//  STAlertView.h
//  STKitDemo
//
//  Created by SunJiangting on 14-8-28.
//  Copyright (c) 2014年 SunJiangting. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface STAlertView : UIView

- (instancetype)initWithMenuTitles:(NSString *)menuTitle, ... NS_REQUIRES_NIL_TERMINATION;

- (NSInteger)showInView:(UIView *)view animated:(BOOL)animated;

@end
[STAlertView.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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
//
//  STAlertView.m
//  STKitDemo
//
//  Created by SunJiangting on 14-8-28.
//  Copyright (c) 2014年 SunJiangting. All rights reserved.
//

#import "STAlertView.h"
#import <STKit/STKit.h>

@interface _STAlertViewCell : UITableViewCell

@property(nonatomic, strong) UILabel *titleLabel;
@property(nonatomic, strong) UIView *separatorView;

@end

const CGFloat _STAlertViewCellHeight = 45;

@implementation _STAlertViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.frame = CGRectMake(0, 0, 320, _STAlertViewCellHeight);
        self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, 300, 44)];
        self.titleLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        self.titleLabel.textColor = [UIColor darkGrayColor];
        self.titleLabel.highlightedTextColor = [UIColor whiteColor];
        self.titleLabel.font = [UIFont systemFontOfSize:17.];
        self.titleLabel.textAlignment = NSTextAlignmentCenter;
        [self addSubview:self.titleLabel];

        self.separatorView = [[UIView alloc] initWithFrame:CGRectZero];
        [self addSubview:self.separatorView];
    }
    return self;
}

- (void)setFrame:(CGRect)frame {
    [super setFrame:frame];
    self.separatorView.frame = CGRectMake(0, CGRectGetHeight(frame) - STOnePixel(), CGRectGetWidth(frame), STOnePixel());
}

- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated {
    [super setHighlighted:highlighted animated:animated];
    self.separatorView.backgroundColor = [UIColor colorWithRed:0x99/255. green:0x99/255. blue:0x99/255. alpha:1.0];
}

@end

@interface STAlertView () <UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate> {
    BOOL _shouldContinue;
    NSInteger _dismissedIndex;
}

@property(nonatomic, strong) NSMutableArray *dataSource;

@property(nonatomic, weak) UITableView *tableView;
@property(nonatomic, weak) UIView *backgroundView;
@property(nonatomic, weak) UIView *contentView;

@end

@implementation STAlertView

- (instancetype)initWithMenuTitles:(NSString *)menuTitle, ... NS_REQUIRES_NIL_TERMINATION {
    NSMutableArray *dataSource = [NSMutableArray arrayWithCapacity:1];
    NSString *title = nil;
    va_list args;
    if (menuTitle) {
        [dataSource addObject:menuTitle];
        va_start(args, menuTitle);
        while ((title = va_arg(args, NSString *))) {
            [dataSource addObject:title];
        }
        va_end(args);
    }
    self = [super initWithFrame:CGRectZero];
    if (self) {
        self.dataSource = dataSource;
        CGFloat maxHeight = 240;
        const CGFloat footerHeight = 1;
        CGFloat height = dataSource.count * _STAlertViewCellHeight + footerHeight;

        UIView *backgroundView = [[UIView alloc] initWithFrame:self.bounds];
        backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        backgroundView.backgroundColor = [UIColor blackColor];
        [self addSubview:backgroundView];
        self.backgroundView = backgroundView;

        UIView *contentView = [[UIView alloc] initWithFrame:CGRectMake(0, STOnePixel(), 0, MIN(height, maxHeight))];
        contentView.clipsToBounds = YES;
        [self addSubview:contentView];
        self.contentView = contentView;

        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dismissActionFired:)];
        tapGesture.numberOfTapsRequired = 1;
        tapGesture.delegate = self;
        [self.backgroundView addGestureRecognizer:tapGesture];

        UITableView *tableView = [[UITableView alloc] initWithFrame:self.contentView.bounds style:UITableViewStylePlain];
        tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        tableView.delegate = self;
        tableView.dataSource = self;
        tableView.backgroundView = nil;
        tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
        [tableView registerClass:[_STAlertViewCell class] forCellReuseIdentifier:@"Identifier"];
        [self.contentView addSubview:tableView];
        self.tableView = tableView;

        tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, footerHeight)];
        tableView.tableFooterView.backgroundColor = [UIColor colorWithRed:0xFF/255. green:0x73/255. blue:0 alpha:1.0];

        if (height > maxHeight) {
            tableView.scrollEnabled = YES;
        } else {
            tableView.scrollEnabled = NO;
        }
    }
    return self;
}

- (NSInteger)showInView:(UIView *)view animated:(BOOL)animated {
    //
    {
        self.tableView.contentInset = UIEdgeInsetsZero;
        if (!view) {
            view = [UIApplication sharedApplication].keyWindow;
            [view addSubview:self];
        } else {
            self.frame = view.bounds;
            [view addSubview:self];
        }

        CGRect frame = self.contentView.frame;
        frame.size.width = CGRectGetWidth(view.bounds);

        self.backgroundView.alpha = 0.0;
        CGRect fromRect = frame, targetRect = frame;
        fromRect.size.height = 0;
        self.contentView.frame = fromRect;
        void (^animation)(void) = ^(void) {
            self.backgroundView.alpha = 0.5;
            self.contentView.frame = targetRect;
        };
        void (^completion)(BOOL) = ^(BOOL finished) {

        };
        if (animated) {
            [UIView animateWithDuration:0.35 animations:animation completion:completion];
        }
    }
    while (!_shouldContinue) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    return _dismissedIndex;
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
    return 0;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.dataSource.count;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return _STAlertViewCellHeight;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    _STAlertViewCell *tableViewCell = [tableView dequeueReusableCellWithIdentifier:@"Identifier"];
    tableViewCell.titleLabel.text = self.dataSource[indexPath.row];
    return tableViewCell;
}

- (void)dismissAnimated:(BOOL)animated {
    [self _dismissAnimated:animated completion:^(BOOL finished){}];
}

#pragma mark - PrivateMethod
- (void)dismissActionFired:(UITapGestureRecognizer *)sender {
    _dismissedIndex = -1;
    [self _dismissAnimated:YES completion:^(BOOL finished){}];
}

- (void)_dismissAnimated:(BOOL)animated completion:(void (^)(BOOL))_completion {
    self.backgroundView.alpha = 0.5;
    CGRect fromRect = self.contentView.frame, targetRect = self.contentView.frame;
    self.contentView.frame = fromRect;
    targetRect.size.height = 0;

    void (^animation)(void) = ^(void) {
        self.backgroundView.alpha = 0.0;
        self.contentView.frame = targetRect;
    };
    void (^completion)(BOOL) = ^(BOOL finished) {
        _shouldContinue = YES;
        if (_completion) {
            _completion(finished);
        }
        [self removeFromSuperview];
    };
    if (animated) {
        [UIView animateWithDuration:0.35 animations:animation completion:completion];
    } else {
        animation();
        completion(YES);
    }
}

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    _dismissedIndex = indexPath.row;
    [self dismissAnimated:YES];
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint touchedPoint = [gestureRecognizer locationInView:self];
    if (touchedPoint.y < CGRectGetMaxY(self.contentView.frame) && touchedPoint.y > CGRectGetMinX(self.contentView.frame)) {
        return NO;
    }
    return YES;
}

@end

说明一下: STOnePixel() 代表1px,由于在其他共有类里面申明的,就不把其他类粘在这里了,方法实现

CGFloat STOnePixel() { return 1.0 / [UIScreen mainScreen].scale; }

PS: 这只是一种技能的get,不代表必须这么做,大部分情况我还是使用异步+Callback的形式来处理类似的问题的。

大家可以在我的github上下载代码的 Demo


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值