iOS | 对封装自定义弹窗的一点思考

作者: Lol刀妹 | 来源:发表于2018-07-19 21:46 被阅读414次
    iu

    背景

    由于项目原因,经常需要封装自定义弹窗。

    最开始我封装自定义弹窗的思路是在[UIApplication sharedApplication].keyWindow上add一个自定义view,后来被keyWindow坑过一次后,改为在[[[UIApplication sharedApplication] delegate] window]上add自定义view。

    按照这种套路,一路走来封装了不少弹窗,未曾觉得有何不妥,直到有一天:

    这是一个弹窗

    如图所示,这个弹窗里共有三个模块(列表),分别对应三个接口。

    平心而论,没有什么弹窗是在[[[UIApplication sharedApplication] delegate] window]上add自定义view解决不了的,但这里我并未立即动手——

    因为它跟我之前封装的弹窗有点不同。

    在这之前,我遇到的弹窗都是比较“死”的,类似于UIAlertView,一个标题,一段描述,以及一两个button。

    我的思考

    如果还是按照以前的思路,将弹窗看做一个自定义view的话,那么需要考虑的就是数据请求那一块:

    方案一:

    我可以在Controller里将三个接口请求完,然后将数据整合成一个model赋值给弹窗。这种思路其实挺OK,挺MVC的,但我就是觉得在Controller里写一大段代码就为了show一个弹窗真的很别扭。

    方案二:

    简单点,把请求数据的代码直接放到弹窗view里,简单易读,通熟易懂,何乐而不为?再者这个数据本来就属于弹窗,弹窗做它自己该做的事也没毛病啊!可是,强迫症患者表示真的不能接受在view里请求数据!

    换个角度看问题

    这时我想到了UIAlertControllerUIAlertController继承自UIViewController,它既是Controller,也是弹窗。

    如果说之前的弹窗都是模仿UIAlertView的话,那么今天不妨来模仿一下UIAlertController

    把这个涉及数据请求的弹窗看做一个Controller的话,瞬间就不纠结了:数据请求什么的直接放弹窗Controller就好了。

    最终效果

    基于上面的思路,我封装了一个继承自UIViewController的弹窗,demo效果如下:

    demo.gif

    从点击筛选按钮到用户完成筛选最后刷新页面,整个过程所对应的代码为:

    #pragma mark - 筛选按钮点击
    
    - (void)filterButtonClicked {
        // 弹窗
        CQFilterAlertController *alertController = [CQFilterAlertController
                                                    alertWithRegionID:_currentRegionID
                                                    departmentID:_currentDepartmentID
                                                    houseID:_currentHouseID
                                                    filtrateCompletion:^(CQRegionModel *selectedRegionModel, CQDepartmentModel *selectedDepartmentModel, CQHouseModel *selectedHouseModel)
        {
            // 筛选完成的回调
            _currentRegionID     = selectedRegionModel.region_id;
            _currentDepartmentID = selectedDepartmentModel.department_id;
            _currentHouseID      = selectedHouseModel.house_id;
            self.regionLabel.text     = selectedRegionModel.region_name;
            self.departmentLabel.text = selectedDepartmentModel.department_name;
            self.houseLabel.text      = selectedHouseModel.house_name;
        }];
        // 弹窗show出来
        [self presentViewController:alertController animated:NO completion:nil];
    }
    

    仿照UIAlertController,两行代码,简洁明了,以present一个模态controller的形式弹出弹窗。

    涉及到的一些知识点

    1.如何弹出半透明模态Controller?

    指定弹窗的modalPresentationStyleUIModalPresentationOverFullScreen即可,当然这需要在初始化的时候指定:

    #pragma mark - 构造方法
    
    + (instancetype)alertWithRegionID:(NSString *)regionID departmentID:(NSString *)departmentID houseID:(NSString *)houseID filtrateCompletion:(FiltrateCompletion)filtrateCompletion {
        CQFilterAlertController *alertController = [[CQFilterAlertController alloc] init];
        alertController.regionID = regionID;
        alertController.departmentID = departmentID;
        alertController.houseID = houseID;
        alertController.filtrateCompletion = filtrateCompletion;
        // 模态
        alertController.modalPresentationStyle = UIModalPresentationOverFullScreen;
        return alertController;
    }
    
    2.如何确定多个接口的数据都已请求完毕?

    可以使用GCD中的信号量:

    #pragma mark - 加载数据
    
    - (void)loadDataSuccess:(void (^)(void))success failure:(void (^)(NSString *errorMessage))failure {
        // 3个接口,全部请求成功后刷新tableView
        NSInteger totalCount = 3;
        __block NSInteger requestCount = 0;
        
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 获取大区列表
            [CQFilterRequest loadRegionArraySuccess:^(NSArray *regionArray) {
                for (NSDictionary *dict in regionArray) {
                    NSError *error = nil;
                    CQRegionModel *model = [[CQRegionModel alloc] initWithDictionary:dict error:&error];
                    if ([model.region_id isEqualToString:self.regionID]) {
                        model.selected = YES;
                        self.selectedRegionModel = model;
                    } else {
                        model.selected = NO;
                    }
                    [self.regionArray addObject:model];
                }
                
                if (++requestCount == totalCount) {
                    dispatch_semaphore_signal(sem);
                }
            } failure:^(NSString *errorMessage) {
                !failure ?: failure(errorMessage);
            }];
            
            // 获取部门列表
            [CQFilterRequest loadDepartmentArraySuccess:^(NSArray *departmentArray) {
                for (NSDictionary *dict in departmentArray) {
                    NSError *error = nil;
                    CQDepartmentModel *model = [[CQDepartmentModel alloc] initWithDictionary:dict error:&error];
                    if ([model.department_id isEqualToString:self.departmentID]) {
                        model.selected = YES;
                        self.selectedDepartmentModel = model;
                    } else {
                        model.selected = NO;
                    }
                    [self.departmentArray addObject:model];
                }
                
                if (++requestCount == totalCount) {
                    dispatch_semaphore_signal(sem);
                }
            } failure:^(NSString *errorMessage) {
                !failure ?: failure(errorMessage);
            }];
            
            // 获取门店列表
            [CQFilterRequest loadHouseArraySuccess:^(NSArray *houseArray) {
                for (NSDictionary *dict in houseArray) {
                    NSError *error = nil;
                    CQHouseModel *model = [[CQHouseModel alloc] initWithDictionary:dict error:&error];
                    if ([model.house_id isEqualToString:self.houseID]) {
                        model.selected = YES;
                        self.selectedHouseModel = model;
                    } else {
                        model.selected = NO;
                    }
                    [self.houseArray addObject:model];
                }
                
                if (++requestCount == totalCount) {
                    dispatch_semaphore_signal(sem);
                }
            } failure:^(NSString *errorMessage) {
                !failure ?: failure(errorMessage);
            }];
            
            dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
            dispatch_async(dispatch_get_main_queue(), ^{
                success();
            });
        });
    }
    
    3.回调方式block&delegate的选择

    block可以做到的,delegate都可以做到。

    这里使用block的原因一是简洁,二是直白

    试想一下,如果这里使用的是delegate会怎样?

    假如另一个开发者来读我的代码,他想找到弹出弹窗相关代码,肯定是先找buttonClicked那个方法,结果发现里面还有一个alertController.delegate,没办法只得跳到alertController的代理方法中继续阅读。明明一个方法就可以搞定的偏偏弄成多个方法,不可不谓之蛋疼。

    有时候,当你拿不准block和delegate选哪一个更好的时候,不妨假象一下:如果我采用xx,会怎样。

    网上有什么说block强调结果,delegate强调过程,还有什么一个对象对应多个事件时用delegate等等。这些,都只能当做参考,也仅仅是参考。

    一定要有自己的思考,包括你读我这篇文章的时候,也要有自己的思考。

    总结:

    封装自定义弹窗,一般来说有两种选择:

    1. [[[UIApplication sharedApplication] delegate] window]上add自定义view;
    2. present一个模态Controller。

    根据你的具体需求做选择。一般而言只是单纯展示或者只涉及少量逻辑处理时可以用自定义view,反之推荐模态Controller。

    完整demo

    https://github.com/CaiWanFeng/iOS_Demo

    题外话

    有人说感觉自己就是个UI码农,每天都在写UI,没意思,没前途。

    言语中透露出写UI很low很低级。我想说,作为前端之一的iOS开发其日常不就是写UI吗?iOS开发不写UI难道写算法?写底层?

    醒醒吧骚年。我倒是觉得你应该反思一下你写UI的功力到底达到了几层。是否连banner都还要copy网上的代码?如果连写UI这门实际开发中最常用最基础的技能都不达标的话就不要好高骛远了。

    前段时间有个读者私信我:

    听他说功能界面都复杂的时候我还是有点虚的,功能界面同时复杂的页面貌似我还真没遇到过。后来他截了一张图过来,发现只是一排菜单栏,点击菜单弹出一个下拉列表框,选中一项后刷新页面,跟我今天的这个demo大同小异。

    虽然在现在的我看来这只是日常开发中的常见需求之一,不值一提,但如果让刚工作三个月的我来做的话,其实也只有在网上抄。

    大部分iOS开发者的起点其实差不多(非计算机专业培训班出身),但是两三年后有些人已经有自己的框架而有些人却还是只会copy别人的代码。

    为什么会有那么大的差距?我想这应该跟每个人对自己的要求有很大关系吧。有些人对自己要求高,遇到问题死磕到底;有些人对自己没什么要求,只要完成需求就好。

    有一点可以肯定的是:如果我遇到问题就去网上copy别人的代码,那我肯定只有一直仰望别人的份。

    我不知道我第一次封装弹窗是什么时候的事了,但我知道自从有了第一次之后,之后各种各样的弹窗都不在话下了。更为重要的是:我不再copy别人的代码,我开始写属于自己的代码

    所以我告诉那个私信我的读者:一定要自己动手做

    当你亲自动手做的时候,自然会有第一手经验,当经验积累到一定程度时,这些问题将不再是问题。

    纸上得来终觉浅,绝知此事要躬行。 —— 陆游

    相关文章

      网友评论

      • 波儿菜:在技术圈,有三种人,一种是伸手党不感冒技术,一种是想精进技术却方法错误,一种是喜欢技术并掌握学习方法。
        而喜欢技术的人中,分两种,一种是背景较好出来就是BAT之类,一种是比较草根挣扎在小公司

        实际上我就是一个热爱技术却背景草根的人,但和作者的技术追求很像,我相信厚积薄发,我相信认真做技术的人不会永远被埋没 :smile:
        Lol刀妹:认真做技术才有翻身的机会。一起加油:sunglasses:
      • 真爱要有你才完美:如果项目时间充足 谁都愿意自己写 同时还能提高自己 我这种公司环境 没办法 光是开发都时间不够了 自测 自测报告 代码review 中间交互调整 资源获取 全是自己跟杭州总部的云端和各种同事要沟通 各种辩论 有时候 不是什么事情都有时间去写 我现在看到感兴趣的第三方会研究 其它的觉得没什么的 为什么就一定自己写呢 不过你的这个还是很好的 自己写的 终归是最好的 以后会努力向你这方面 转向
        Lol刀妹:确实,每个人所处的开发环境不一样。按时完成任务肯定是最高优先级,再这之后尽量想办法提升自己的编码功底。
      • RockerSean:喜歡樓主的文章! 很認同你說的「我不再copy别人的代码,我开始写属于自己的代码」。做了手機軟件開發5年了,經常聽到同事說:「有library啊,直接拿來可以用」 「這個功能很難實現,已經是別人的library寫好了」... 聽到這些我通常都是不屑一顧,自己想想應該怎麼實現這些功能,看看類似於樓主這種優秀的文章,然後接下來就是自己去自定義相關的功能。確實遇到很多人覺得手機軟件開發就只是寫UI而已,但其實有經驗的和沒經驗的區別一眼就看得出。有些自以為實現了功能的放到不同的屏幕上一跑就爛,每到新設備出來就不得不強制更新。所以說,還是應該自己多思考,就像樓主說的block和delegate,沒有什麼絕對的事情,MVC,MVVM,MVP各種模式都有自己的優缺點。
        Lol刀妹:@RockerSean 是的,个人认为主动思考的意识非常重要。
      • 呼呼兔:为什么不用UIPresentationController实现呢?
        Lol刀妹:因为没有仔细研究过:sweat_smile:
      • 米修斯_:同道中人
        Lol刀妹:握手握手
      • Leaf_秋天:我对第一张图片更感兴趣
        Lol刀妹:@Leaf_秋天 拿走不谢
      • PGOne爱吃饺子:那个弹窗不是用UIAlertController ,最后怎么有用到了 UIViewController
        Lol刀妹:@PGOne爱吃饺子 不给的话,ViewController就不是半透明的了
        PGOne爱吃饺子:@无夜之星辰 这一句话的主要作用是干什么的,指定弹窗的modalPresentationStyle 为UIModalPresentationOverFullScreen。
        不给可不可以啊
        Lol刀妹:UIAlertController不能被继承,所以只有参考UIAlertController的实现原理。
      • PGOne爱吃饺子:关注了好多iOS大神吧,其实最喜欢看的就是你的文章,你写的文章里面会讲的很细,有哪些注意的地方,这种方式为什么不行,两种方法的比较,不好高骛远,加油。
        Lol刀妹:哈哈,刚把得:sunglasses:
      • 请叫我小白同学:搬板凳,听师傅敲重点,自己手敲代码!
        Lol刀妹:@请叫我小白同学 :joy:
      • 心语风尚:弹框大小怎么改呢
        Lol刀妹:@心语风尚 修改对应约束即可

      本文标题:iOS | 对封装自定义弹窗的一点思考

      本文链接:https://www.haomeiwen.com/subject/xiivpftx.html