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]123456789101112131415161718192021222324 |
- (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是可以接受用户触摸事件的。所以答案揭晓,按钮的点击动作可以执行,点击效果可以显示,我们看一下点击三次测试按钮的打印的日志,后续会附带源码下载地址
[打印日志]1234 |
2014-10-27 15:29:36.796 STRunloop[3474:195616] Did Start viewDidAppear2014-10-27 15:29:39.493 STRunloop[3474:195616] TestButtonActionFired2014-10-27 15:29:40.487 STRunloop[3474:195616] TestButtonActionFired2014-10-27 15:29:40.902 STRunloop[3474:195616] TestButtonActionFired |
代码如预期执行,没有错的,这样看来,我们现在需要做的就是保证事件机制的正常运行,以及block住当前代码就可以了,根据上面的知识,我们可以得到一个方法原型
[ShowInView]1234567 |
- (NSInteger)showInView:(UIView *)view animated:(BOOL)animated { // while (!_shouldContinue) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } return _dismissedIndex;} |
我们只需要在用户点击完完成按钮之后,为_dismissedIndex和_shouldContinue赋值就可以了。到这里之后,我们差不多就已经实现了一个Block执行的AlertView,剩余的代码都不复杂,我就直接贴代码了
[STAlertView.h]1234567891011121314151617 |
//// 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 |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
//// 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;@endconst 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