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

百家 作者:iOS开发 2018-07-22 11:33:59

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!















































































































































































































































































































    先不说楚枫的这般年纪,能够踏入元武一重说明了什么,最主要的是,楚枫在刚刚踏入核心地带时,明明只是灵武七重,而在这两个月不到的时间,连跳两重修为,又跳过一个大境界,踏入了元武一重,这般进步速度,简直堪称变态啊。


    “这楚枫不简单,原来是一位天才,若是让他继续成长下去,绝对能成为一号人物,不过可惜,他太狂妄了,竟与龚师兄定下生死约战,一年时间,他再厉害也无法战胜龚师兄。”有人认识到楚枫的潜力后,为楚枫感到惋惜。


    “哼,何须一年,此子今日就必败,巫九与龚师兄关系甚好,早就看他不顺眼了,如今他竟敢登上生死台挑战巫九,巫九岂会放过他?”但也有人认为,楚枫今日就已是在劫难逃。


    “何人挑战老子?”就在这时,又是一声爆喝响起,而后一道身影自人群之中掠出,最后稳稳的落在了比斗台上。


    这位身材瘦弱,身高平平,长得那叫一个猥琐,金钩鼻子蛤蟆眼,嘴巴一张牙带色儿,说话臭气能传三十米,他若是当面对谁哈口气,都能让那人跪在地上狂呕不止。


    不过别看这位长得不咋地,他在核心地带可是鼎鼎有名,剑道盟创建者,青龙榜第九名,正是巫九是也。


    “你就是巫九?”楚枫眼前一亮,第一次发现,世间还有长得如此奇葩的人。


    巫九鼻孔一张,大嘴一咧,拍着那干瘪的肚子,得意洋洋的道:“老子就是巫九,你挑战老子?”


    “不是挑战你,是要宰了你。”楚枫冷声笑道。


    “好,老子满足你这个心愿,长老,拿张生死状来,老子今日在这里了解了这小子。”巫九扯开嗓子,对着下方吼了一声。


    如果他对内门长老这么说话,也就算了,但是敢这么跟核心长老说话的,他可真是算作胆肥的,就连许多核心弟子,都是倒吸了一口凉气,心想这楚枫够狂,想不到这巫九更狂。


    不过最让人无言的就是,巫九话音落下不久,真有一位核心长老自人群走出,缓缓得来到了比斗台上,左手端着笔墨,右手拿着生死状,来到了巫九的身前。


    “我去,这巫九什么身份,竟能这般使唤核心长老?”有人吃惊不已,那长老修为不低,乃是元武七重,比巫九还要高两个层次,但却这般听巫九的话,着实让人吃惊不已。


    “这你就不知道了吧,巫九在前些时日,拜了钟离长老为师尊,已正式得到钟离长老的亲传。”有人解释道。


    “钟离长老?可是那位性情古怪的钟离一护?”


    “没错,就是他。”


    “天哪,巫九竟然拜入了他的门下?”


    人们再次大吃一惊,那钟离一护在青龙宗可是赫赫有名,若要是论其个人实力,在青龙宗内绝对能够排入前三,连护宗六老单打独斗都不会是他的对手。


    只不过那钟离一护,如同诸葛青云一样,也是一位客卿长老,所以在青龙宗内只是挂个头衔,什么事都不管,更别说传授宗内弟子技艺了,如今巫九竟然能拜入他老人家门下,着实让人羡慕不已。


    “恩怨生死台,的确可以决斗生死,但必须要有所恩怨,你们两个人,可有恩怨?”那位长老开口询问道。































































































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

土味iOS讲义  https://github.com/Mr-Wei/earthy-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  on which is already presenting ,那么这个弹窗就相当于被吃掉了。


不同风格的梦想导师


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


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


"右派"


既然说到需要有一个队,那么我们实际操作起来第一件事情肯定是要先创建一个队列,而且这个队列应该是全局的,单例的,整个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

  • iOS开发整理发布,转载请联系作者获得授权

【点击成为源码大神】

关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接