71

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

 5 years ago
source link: http://www.10tiao.com/html/276/201807/2655359900/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

点击上方“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 <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

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

【点击成为源码大神】


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK