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

老子叫甜甜 提交于 2020-03-02 17:14:06


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
[STAlertView.m]
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


易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!