39

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

 5 years ago
source link: http://www.cocoachina.com/ios/20180720/24243.html?amp%3Butm_medium=referral
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.

IF3qiyb.jpg!web

iu

背景

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

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

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

MFj2u2B.png!web

这是一个弹窗

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

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

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

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

我的思考

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

方案一:

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

方案二:

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

换个角度看问题

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

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

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

最终效果

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

3UJ73in.gif

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?

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

#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) {
            if (++requestCount == totalCount) {
                dispatch_semaphore_signal(sem);
            }
            
            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];
            }
        } failure:^(NSString *errorMessage) {
            !failure ?: failure(errorMessage);
        }];
        
        // 获取部门列表
        [CQFilterRequest loadDepartmentArraySuccess:^(NSArray *departmentArray) {
            if (++requestCount == totalCount) {
                dispatch_semaphore_signal(sem);
            }
            
            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];
            }
        } failure:^(NSString *errorMessage) {
            !failure ?: failure(errorMessage);
        }];
        
        // 获取门店列表
        [CQFilterRequest loadHouseArraySuccess:^(NSArray *houseArray) {
            if (++requestCount == totalCount) {
                dispatch_semaphore_signal(sem);
            }
            
            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];
            }
        } 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这门 实际开发中最常用最基础 的技能都不达标的话就不要好高骛远了。

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

imquaiB.png!web

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

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

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

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

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

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

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

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

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

作者:无夜之星辰

链接:https://www.jianshu.com/p/541a46d867f9#comments


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK