iOS土味儿讲义(二)--弹窗的前世今生

2018 年 7 月 19 日 CocoaChina

这是我的土味iOS讲义的第二篇,完整项目的github地址:

土味iOS讲义


整个系列龟速更新中,觉得有意思的请点下 Star,有疑问或者任何想法和建议欢迎提 Issues。


另外,上一篇的作业有人做吗?

开始之前先对上一篇《一个Button引发的血案》的一些疑问做一些总结说明。


  • 这整个一系列的文章里面,所有的需求都是我编的,可能有人在自己过往的项目中遇到过类似的,也有可能没遇到过甚至永远不会用到。你可能觉得需求很荒谬,但请不要在意这些细节,因为原本文章的目的就不是来讲解如何完成某一个需求的。

  • 很多人都知道KVO是基于runtime实现的,甚至有人能够自己动手实现。但是这个知识点你学完了之后除了能用他来实现KVO之外还能做什么吗?恐怕大部分的人都一脸问号。如果你看完上一篇文章,能把问号变成“卧槽!还有这种操作???”,那我的目的就达到了。

  • 实名反对runtime什么的,在你把runtime的源码都看一遍之后再说会更有说服力。


开始说说弹窗


当我们每次接到一个不知道怎么去实现的需求的时候,仿照系统原生的写法是最好的解决方案,所以聊起弹窗的话,UIAlertview这个控件是怎么都避不开的话题,因为没有哪一个弹窗的设计比UIAlertView更经典了,想写好一个优秀的自定义弹窗,那么抄他准没错。


但是任何设计的优秀性都是具有时效的,苹果对于Alert的实现方案也不是一成不变的。


UIAlertView的前世今生


在远古到上古时代(iOS 7以前),UIAlertView是通过在屏幕上加了一层Alertwindow, 然后将AlertView的视图加在了这个Window上,在很多页面跳转的总结文章或者第三方框架中都是使用这样的方法。类似于以下的代码你肯定见过:

- (void)showAlertView:(UIView*)view{
    _window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    _window.windowLevel = UIWindowLevelAlert;
    _window.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.6];
    [_window addSubview:view];
    _window.hidden = NO;
    [_window makeKeyAndVisible];
}


当然还有变种的ViewController版本:

- (void)showAlertViewController:(UIViewController*)viewController{
    _window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
    _window.windowLevel = UIWindowLevelAlert;
    _window.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.6];
    _window.rootViewController = viewController;
    _window.hidden = NO;
    [_window makeKeyAndVisible];
}


很好很强大的思路,在大家做个页面跳转都一脸懵逼无脑Push的上古时代,这绝对是页面弹窗的最佳实现无疑了,因为苹果自己就是这么写的。


上古时代的腾讯QQ第三方登录SDK,在你手机内没有安装QQ的时候,会从左边滑出一个网页的QQ登录页面,其实也是这种创建window的做法。(如果不是,请指正。)


到了中古时代(iOS 8 以前),苹果已经不推荐我们自己创建新的window来实现弹窗功能了,或许是因为滥用window导致了一些他不想看到的后果?谁知道呢!反正在整个中古时代,UIAlertView是直接贴在当前的keywindow上的,不再创建新的window来放置弹窗,从这个时候开始,window的隐藏和显示,还有关于windowLevel的一切,我们可以不用再过于关心了。


及至近古时代(iOS 8之后),UIAlertview以及他的小兄弟UIActionSheet在经历了漫长的岁月之后,终于寿终正寝。UIAlertController横空出世,取而代之。从这个时候开始,苹果对弹窗的定义终于从一个view层级上升到了controller层级,在减少对window层级的暴力操作的同时,增强了对弹窗整个生命周期的把控。


为什么要说历史?


可能很多人都不是很在意这方面的问题,因为他看起来好像没有什么用处,但是我还是觉得了解这些东西是有必要的,至少如果说一个比较晚入门的iOS开发者,可能没有见过那些老式的写法,也从来没有直接操作过window层级的东西,在见到相关的代码的时候,能够有一个正确的判断,知道他是已经过时的东西。


最起码我觉得不要以后有一天,会有人发一篇文章说:卧槽现在的iOS面试者,让他写一个自定义弹窗他还要创建一个Window,这种写法现在还有人在用我也是服了!阮一峰老师已经被黑的够惨了,所以最好不要留给别人以后鞭尸我们的机会。


做一个普通的弹窗


现在已经很少有公司还去写兼容iOS 7系统的代码了,在苹果官方的统计数据是这样的:



使用iOS 9以及更低版本的用户只有5%,这里面如果再去除iOS 8 和iOS 9 的使用率,留给 iOS 7的份额还有多少我不太好预测,但是趋势明眼人都能看得出来,如果你的公司还要一意孤行的让你去兼容iOS 7的话,甩出这张图去跟他撕吧。


当然其实我更倾向于直接基于iOS 9系统去做开发,那么苹方字体就不用兼容了,但是目前还没能成功的说服某些人。


所以那么显而易见的,既然至少基于iOS 8来开发,那么弹窗就应该使用UIAlertController的思路来实现咯,根本就不需要采用第二种办法,跟着苹果的方向走肯定是没有错的,那么我们开始吧!


1. 首先当然还是要来看看,如果我们想在iOS 8以上的系统里面使用UIAlertController是怎么样的,一般的写法像下面这样:

UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"弹窗" message:@"消息" preferredStyle:UIAlertControllerStyleAlert];
    UIAlertAction *action1 = [UIAlertAction actionWithTitle:@"はい" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    }];
    [alertController addAction:action1];
    UIAlertAction *action2 = [UIAlertAction actionWithTitle:@"いいえ" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    }];
    [alertController addAction:action2];
    [self presentViewController:alertController animated:YES completion:nil];


我们抛开UIAlertAction相关的代码看一看,emmm这TM不就是创建了一个ViewController,然后present出来了吗?我也可以啊!


所以先写一个ViewController,长什么样子无所谓,看起来像个弹窗就行,弹窗周边应该是透明的,很自然的把self.view的背景色设置为透明,然后随便写个触发事件把他present出来就行了。但是当我们做完这一步的时候,好像发生了什么奇奇怪怪的事情:



弹出来的窗口整个黑掉了,说明我们之前某些地方是想错了的,present出来的viewController表面上看起来是盖在了原有界面的上面,但那其实只是个动画,最终实际上是替换掉了,所以设置透明以后会导致下面没有东西所以就变成黑色了。


所以我们需要重写viewController的初始化方法,添加self.modalPresentationStyle = UIModalPresentationCustom;改变一下present方式,就可以用覆盖的方式来present新的界面了。


至于说弹出窗口的方式,那就需要自定义转场动画了,无论你想要左弹,右弹,飞入还是溶解都不是问题,这些地方我都不想说太多,具体可以参考一下我很久之前写过的仿UIAlertController实现


这些内容其实几年前就有人写过了,所以不太想再贴一堆系统属性,枚举等等一系列代码,讲解每一个属性有什么效果,感觉意义不大,一笔带过就好,最后总结一下自己的看法:


  • 就像前面所说的一样,苹果选择推出UIAlertController除了避免直接操作UIWindow之外,更加强了对整个弹窗的生命周期的把控,这句话可以琢磨琢磨。

  • 弹窗是全屏的,虽然看起来好像有用的只有中间那一点点,但这种实现方式本身就已经把整个屏幕的操作空间都预留出来了,你可以尽情发挥。

  • 那种左滑半个屏幕,侧弹一个选项卡的操作其实殊途同归,一切的不同只不过就在于转场动画和弹窗的UI设计罢了,本质上是一样的。

  • 弹窗继承自UIViewController但是并不仅限于此,可以带Navgation可以带Tabbar,一切任你想象。比方说要做一个登录窗口,窗口还要支持跳转输入验证码?如果使用UIView来做的话,是不是想一想都觉得很恐怖?

  • 个人不太推荐现在仍然还在自行创建Window甚至在某个单例下面常年挂着一个Window的写法,但是向当前KeyWindow上addSubView这种写法其实在某些情况下也是可以小而美的,可以根据情况选择大可不必矫枉过正。


普通的弹窗讲完了,那么如何让弹窗也能实现自己的窗生价值,变得不普通呢?


如果弹窗也有梦想


很显然,能够让用户看到是弹窗实现自己窗生价值的唯一途径,但是很遗憾,一个APP里面弹窗有很多,位置却只有一个,所以排除所有其他弹窗所造成的干扰,将自己显示在用户的面前,我称之为弹窗的梦想。


在正常的情况下,一个优秀的设计是能够让所有的弹窗在互不影响的情况下,实现各自的价值的。但是理想与现实总是有差距的,在我们的APP里面除了像输入框,登录窗那样的弹窗之外还有一种比较特别的,我称之为触发式弹窗。这些弹窗往往是不可控的,我们不知道他什么时候会弹出来,这种弹窗弹出的时机往往取决于外部:后台服务器消息发送,手机信号强弱变化,电量变化等等。


总之生活中就是有这么多的巧合,然而不论是巧合也好,单纯的设计缺陷也罢,都不是我们置之不理的理由,终究还是需要一一应对,那么现在来看看造成这种现象的原因吧!


首先我要把锅甩给UIAlertController了,因为自从他出现开始,才有这种问题的,或者说不是UIAlertController的问题,是presentViewController这种实现方式的局限性,而这种局限性在UIAlertView时代是被规避了的。


有兴趣的可以去做一个实验,写一个方法连续show两个不同的UIAlertView,其中一个会在另一个dissmiss之后自动显示出来,而如果你连续present两个UIAlertController,那么第二次的present其实是无效的,控制台会输出类似于这样的Warning: Attempt to present <UIAlertController: 0x7fb07582ce00>  on <ViewController: 0x7fb074608450> which is already presenting <UIAlertController: 0x7fb075803000>,那么这个弹窗就相当于被吃掉了。


不同风格的梦想导师


如果说弹窗是有梦想的,那么我们程序员自然就是他们的梦想导师,为他们在追寻梦想的道路上保驾护航。导师有两种,一种比较保守,我称之为“右派”;一种比较激进,我称之为“左派”。简单的说来,“右派”导师喜欢排队,"左派"导师喜欢插队,两种风格需要根据不同的设计需求来选择。


所以,首先你要有一个队。


"右派"


既然说到需要有一个队,那么我们实际操作起来第一件事情肯定是要先创建一个队列,而且这个队列应该是全局的,单例的,整个APP所有的弹窗都需要通过这个队列来管理,那么既然presentViewController是ViewController的工作,我选择创建一个ViewController的类别,添加这样的方法:

- (NSOperationQueue *)getOperationQueue {    
    static NSOperationQueue *operationQueue = nil;    
    static dispatch_once_t onceToken;    
    dispatch_once(&onceToken, ^{        
        operationQueue = [NSOperationQueue new];        
    });    
    return operationQueue;
}


以上代码添加一个方法获取一个单例的队列,用来存放所有的弹窗操作,想要使用队列管理所有弹窗行为,就要使用operation将弹窗操作包裹起来,并设置操作依赖,让其中一个弹窗完成之后,才允许新的弹窗弹出,创建一个自定义的presentViewController方法来实现以上需求:

- (void)xxx_presentViewController:(UIViewController *)controller completion:(void (^)(void))completion{
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        dispatch_async(dispatch_get_main_queue(), ^{ 
            [self presentViewController:controller animated:YES completion:completion];
        });
    }];
    if ([self getOperationQueue].operations.lastObject) {
        [operation addDependency:[self getOperationQueue].operations.lastObject];
    }
    [[self getOperationQueue] addOperation:operation];  
}


这样我们就将弹窗操作排列了起来,但弹窗并不是一个子线程的耗时操作,真正的弹窗动作最终还是要转到主线程来做,整个operation其实在弹窗弹出来的那个瞬间就已经结束了,所以我们应该想一个方法将队列线程阻塞住,就好像将弹窗的位置当做一个公共资源来访问,只有在当前弹窗位置没有窗口的时候才允许弹窗,这里我选择使用dispatch_semaphore_t(信号量)。


将上面的代码变化一下:

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);    
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{        
        dispatch_async(dispatch_get_main_queue(), ^{            
            [self presentViewController:controller animated:YES completion:completion];            
        });        
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);        
    }];
    if ([self getOperationQueue].operations.lastObject) {        
        [operation addDependency:[self getOperationQueue].operations.lastObject];        
    }    
    [[self getOperationQueue] addOperation:operation];


创建一个初始值为0的信号量semaphore,在弹窗弹出之后调用dispatch_semaphore_wait会一直阻塞线程使得下一个弹窗操作不会被执行,剩下的只需要在弹窗消失的时候调用dispatch_semaphore_signal将信号量+1就好了。


所以我又要使用runtime了:

- (void)setDisappearCompletion:(void (^)(void))completion {
    objc_setAssociatedObject(self@selector(getDisappearCompletion), completion, OBJC_ASSOCIATION_COPY_NONATOMIC);   
}
- (void (^)(void))getDisappearCompletion {    
    return objc_getAssociatedObject(self, _cmd);    
}
+ (void)load {   
    SEL oldSel = @selector(viewDidDisappear:);    
    SEL newSel = @selector(xxx_viewDidDisappear:);    
    Method oldMethod = class_getInstanceMethod([self class], oldSel);    
    Method newMethod = class_getInstanceMethod([self class], newSel);            
    BOOL didAddMethod = class_addMethod(self, oldSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));    
    if (didAddMethod) {        
        class_replaceMethod(self, newSel, method_getImplementation(oldMethod), method_getTypeEncoding(oldMethod));        
    } else {        
        method_exchangeImplementations(oldMethod, newMethod);        
    }    
}
- (void)xxx_viewDidDisappear:(BOOL)animated {    
    [self xxx_viewDidDisappear:animated];
    if ([self getDisappearCompletion]) {
        [self getDisappearCompletion]();        
    }    
}

为UIViewController创建一个block属性,然后hook一下viewDidDisappear方法,让ViewController在消失的时候执行一下回调,最后我们在弹窗操作的operation中设置一下block的内容:

[controller setDisappearCompletion:^{
            dispatch_semaphore_signal(semaphore);
        }]
;


在代码中将信号量+1,使得整个线程由阻塞态变为就绪态,迎接下一个弹窗操作的到来。


这样,“右派”导师所有的特点都被我们确定完毕了,里面使用到了GCD和runtime中

的Method Swizzling,和上一篇文章中的不同。当然Method Swizzling我也是随手就写了一个,不要太过于在意这是不是最安全的写法,不过我对于在信号量中是否使用DISPATCH_TIME_FOREVER尚存疑虑,如果你们谁有比较完整的观点,告诉我好吗?


结语


还是我的一贯作风,“右派”的写法我讲完了,“左派”的风格其实大同小异,有人有兴趣实现一下吗?还是像上一篇文章一样,当做一个作业吧,如果有任何想法,欢迎随时来交流哟。


作者:Mr_Wei

链接:https://juejin.im/post/5ab360d66fb9a028d14101e7


相关推荐:


登录查看更多
0

相关内容

Microsoft Windows(视窗操作系统)是微软公司推出的一系列操作系统。它问世于1985年,当时是DOS之下的操作环境,而后其后续版本作逐渐发展成为个人电脑和服务器用户设计的操作系统。
专知会员服务
42+阅读 · 2020年7月7日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
【MIT】Yufei Zhao《图论与加法组合学》,177页pdf
专知会员服务
49+阅读 · 2020年4月27日
【机器学习课程】Google机器学习速成课程
专知会员服务
164+阅读 · 2019年12月2日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
AWVS12 V12.0.190530102 windows正式版完美破解版
黑白之道
29+阅读 · 2019年8月24日
渗透某德棋牌游戏
黑白之道
12+阅读 · 2019年5月17日
已删除
架构文摘
3+阅读 · 2019年4月17日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
iOS自定义带动画效果的模态框
CocoaChina
7+阅读 · 2019年3月3日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
递归神经网络
Datartisan数据工匠
4+阅读 · 2018年8月2日
tensorflow LSTM + CTC实现端到端OCR
数据挖掘入门与实战
8+阅读 · 2017年11月15日
Advances in Online Audio-Visual Meeting Transcription
Arxiv
4+阅读 · 2019年12月10日
Revealing the Dark Secrets of BERT
Arxiv
4+阅读 · 2019年9月11日
Conditional BERT Contextual Augmentation
Arxiv
8+阅读 · 2018年12月17日
Large-Scale Study of Curiosity-Driven Learning
Arxiv
8+阅读 · 2018年8月13日
Auto-Context R-CNN
Arxiv
4+阅读 · 2018年7月8日
Arxiv
5+阅读 · 2018年6月5日
VIP会员
相关VIP内容
专知会员服务
42+阅读 · 2020年7月7日
【2020新书】使用高级C# 提升你的编程技能,412页pdf
专知会员服务
57+阅读 · 2020年6月26日
【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
117+阅读 · 2020年5月10日
【MIT】Yufei Zhao《图论与加法组合学》,177页pdf
专知会员服务
49+阅读 · 2020年4月27日
【机器学习课程】Google机器学习速成课程
专知会员服务
164+阅读 · 2019年12月2日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
22+阅读 · 2019年11月7日
相关资讯
AWVS12 V12.0.190530102 windows正式版完美破解版
黑白之道
29+阅读 · 2019年8月24日
渗透某德棋牌游戏
黑白之道
12+阅读 · 2019年5月17日
已删除
架构文摘
3+阅读 · 2019年4月17日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
基于Web页面验证码机制漏洞的检测
FreeBuf
7+阅读 · 2019年3月15日
iOS自定义带动画效果的模态框
CocoaChina
7+阅读 · 2019年3月3日
React Native 分包哪家强?看这文就够了!
程序人生
13+阅读 · 2019年1月16日
递归神经网络
Datartisan数据工匠
4+阅读 · 2018年8月2日
tensorflow LSTM + CTC实现端到端OCR
数据挖掘入门与实战
8+阅读 · 2017年11月15日
相关论文
Advances in Online Audio-Visual Meeting Transcription
Arxiv
4+阅读 · 2019年12月10日
Revealing the Dark Secrets of BERT
Arxiv
4+阅读 · 2019年9月11日
Conditional BERT Contextual Augmentation
Arxiv
8+阅读 · 2018年12月17日
Large-Scale Study of Curiosity-Driven Learning
Arxiv
8+阅读 · 2018年8月13日
Auto-Context R-CNN
Arxiv
4+阅读 · 2018年7月8日
Arxiv
5+阅读 · 2018年6月5日
Top
微信扫码咨询专知VIP会员