常见的个人设置列表模块的重构(实现任何需求轻松维护)

转存失败,建议直接上传图片文件

1.背景

项目中的个人设置页面有一个 tableview ,每个列表项对应项目的不同功能,这些列表项会随着项目的需求、用户的角色等多种原因而增加、删除、改变顺序、显示或隐藏。每次需求变动都需要修改大量逻辑复杂的代码。

2.需求

  • 常见的 tableview
  • 每项需要显示不同颜色的图标、标题
  • 实现每项的点击事件
  • 实现分组效果
  • 后期会出现增加删除项、改变顺序、显示或隐藏、不同用户显示不同的项等需求

3.修改前

之前使用的是最简单传统的方式,即分别定义了标题、图标、颜色三个数组来存放列表项的数据,并且每个数组的数据存放顺序必须要保持严格统一。可以看到现在已经被改得面目全非了:

NSArray *iconArray = @[//@"\U0000e63e",
                           @"\U0000E675",
                           /*@"\U0000e641",
                           @"\U0000e673",*/
                           @"\U0000e6a8",
                           @"\U0000e642",
                           @"\U0000e643",
//                           @"\U0000e645",
                           @"\U0000e644",
                           @"\U0000e63f",
                           @"\U0000e61c"];
    NSArray *iconColorArray = @[//Color16Hex(0xFF7875),
                                Color16Hex(0xFF9C6E),
                                /*Color16Hex(0xFF9C6E),
                                Color16Hex(0xFF9C6E),*/
                                Color16Hex(0x316C2E),
                                Color16Hex(0xFFC069),
                                Color16Hex(0xFFD666),
//                                Color16Hex(0xBAE637),
                                Color16Hex(0x95DE64),
                                Color16Hex(0x5CDBD3),
                                Color16Hex(0x69C0FF)];
    NSArray *titleArray = @[//IEText(@"my_exhibition_card"),
                            IEText(@"my_pre_registration"),
                            /*IEText(@"my_scan"),
                            IEText(@"my_schedule_activities"),*/
                            IEText(@"my_intention_order"),
                            IEText(@"my_business_cycle"),
                            IEText(@"my_company_management"),
//                            IEText(@"my_telephone_service"),
                            IEText(@"my_online_service"),
                            IEText(@"my_person_setting"),
                            IEText(@"my_collection")];

通过 for 循环遍历任意数组,将相应的数据取出并构造成一个 cellModel 加入一个数组,接着再根据下标分组,并将这些数组再加入到一个数组中,构成一个二维数组。代码中包含各种需求的判断,这块是最混乱的地方:

NSMutableArray *allArray = [[NSMutableArray alloc] initWithCapacity:4];
NSMutableArray *itemsArray = [[NSMutableArray alloc] initWithCapacity:2];
for (NSInteger i = 0 ; i < iconArray.count; i++) {
    if (i != 3 || [IECache isExhibitor]) {
        if(i != 0 || ![IECache isExhibitor]){
            // 展商:包含公司管理,观众:没有。
            IEMyListCellModel *model = [[IEMyListCellModel alloc] init];
            model.iconString = iconArray[i];
            model.iconColor = iconColorArray[i];
            model.title = titleArray[i];
            [itemsArray addObject:model];
        }
    }

    if ([IECache isExhibitor]) {
        if (i == 1 || i == 3 || i == 4 || i == 6) {
            // 数据分组
            if (itemsArray.count > 0) {
                [allArray addObject:[itemsArray copy]];
                [itemsArray removeAllObjects];
            }
        }
    } else {
        if (i == 0 || i == 2 || i == 4 || i == 6) {
            // 数据分组
            if (itemsArray.count > 0) {
                [allArray addObject:[itemsArray copy]];
                [itemsArray removeAllObjects];
            }
        }
    }
}

接着在 tableView 的各个代理方法中使用 ifelse 一个一个地判断 index 来处理相应列表项的显示和点击事件,下面的代码仅仅是 didSelectRowAtIndexPath 的实现:

UIViewController *vc;
    switch (indexPath.section) {
        case 0: {
            [self _dealInformationCardBlock];
            [IEDataFinderManager eventId:@"myPage_myProfile" attributes:nil];
        }
            break;
        case 1: {
            if (indexPath.row == -1) {
                // 胸卡
                if (USER_IS_VISITOR && (VISITOR_INCOMPLETE_REGISTRATION || [IECache visitorRegistrationVerifyState] == NO)) {
                    [self visitorPreregistrationInApp];
                    return;
                }
                // 展商,未报名时,功能限制
                if (!USER_IS_VISITOR && [IECache exhibitorRegistrationStatus] != YES) {
                    [self showPlainTextPrompt:IEText(@"common_exhibitor_no_apply_alert")];
                    return;
                }
                [self toVisitorCard];
                return;
//            } else if (indexPath.row == 1) {
//                // 扫一扫
//                [self toScanfQRCode];
//                return;
//            } else if (indexPath.row == 2) {
//                // 我的日程活动
//                [self toMyActivityList];
//                return;
            } else {
                if ([IECache isExhibitor]) {
                    // 意向订单管理
                    // 游客模式限制功能
                    if ([IECache touristIsLogin] && ![IECache userIsLogin]) {
                        [self visitorPreregistrationInApp];
                        return;
                    }
                    [self toOrderManager];
                } else {
                    // 游客模式限制功能
                    if ([IECache touristIsLogin] && ![IECache userIsLogin]) {
                        [self visitorPreregistrationInApp];
                        return;
                    }
                    // 为他人预登记
                    vc = [[IEPreRegistrationViewController alloc]init];
                    vc.hidesBottomBarWhenPushed = true;
                    [self.navigationController pushViewController:vc animated:YES];
                }
                return;
            }
            break;
        }
        case 2: {
            if (indexPath.row == 0) {
                if ([IECache isExhibitor]) {
                    // 商脉圈
                    // 观众,未完成预登记,功能限制
                    if (USER_IS_VISITOR  && (VISITOR_INCOMPLETE_REGISTRATION || [IECache visitorRegistrationVerifyState] == NO)) {
                        [[IETool getCurrentVC] visitorPreregistrationInApp];
                        return;
                    }
                    // 展商,未报名时,功能限制
                    if (!USER_IS_VISITOR && [IECache exhibitorRegistrationStatus] != YES) {
                        [self showPlainTextPrompt:IEText(@"common_exhibitor_no_apply_alert")];
                        return;
                    }
                    [IEDataFinderManager eventId:@"myPage_moment" attributes:nil];
                    vc = [[IEBusinessMomentViewController alloc] init];
                } else {
                    // 意向订单管理
                    // 游客模式限制功能
                    if ([IECache touristIsLogin] && ![IECache userIsLogin]) {
                        [self visitorPreregistrationInApp];
                        return;
                    }
                    [self toOrderManager];
                }
            } else {
                if ([IECache isExhibitor]) {
                    // 公司管理
                    // 展商,未报名时,功能限制
                    if ([IECache exhibitorRegistrationStatus] != YES) {
                        [self showPlainTextPrompt:IEText(@"common_exhibitor_no_apply_alert")];
                        return;
                    }
                    [IEDataFinderManager eventId:@"myPage_companyManagement" attributes:nil];
                    vc = [[IECompanyManageViewController alloc] init];
                } else {
                    // 商脉圈
                    // 观众,未完成预登记,功能限制
                    if (USER_IS_VISITOR  && (VISITOR_INCOMPLETE_REGISTRATION || [IECache visitorRegistrationVerifyState] == NO)) {
                        [[IETool getCurrentVC] visitorPreregistrationInApp];
                        return;
                    }
                    // 展商,未报名时,功能限制
                    if (!USER_IS_VISITOR && [IECache exhibitorRegistrationStatus] != YES) {
                        [self showPlainTextPrompt:IEText(@"common_exhibitor_no_apply_alert")];
                        return;
                    }
                    [IEDataFinderManager eventId:@"myPage_moment" attributes:nil];
                    vc = [[IEBusinessMomentViewController alloc] init];
                }
            }
            break;
        }
        case 3: {
            if (indexPath.row == 0) {
//                // 电话客服
//                [EFCustomerServiceTool callTelephoneService];
//                [IEDataFinderManager eventId:@"myPage_customerService" attributes:nil];
                // 在线客服
                [EFCustomerServiceTool callOnlineServiceBlock:^(EFCustomerServiceModel * _Nonnull model) {
                    [IEDataFinderManager eventId:@"myPage_onlineCustomerService" attributes:nil];
                     NSString *easemobAccount = model.easemobAccount ?: @"";
                     NSString *name = model.name ?: @"";
                     NSString *headPortrait = model.headPortrait ?: @"";
                    [self enterChatroomWithSessionId:easemobAccount targetName:name targetAvatar:headPortrait];
                }];
                return;
            } else {
                // 在线客服
                [EFCustomerServiceTool callOnlineServiceBlock:^(EFCustomerServiceModel * _Nonnull model) {
                    [IEDataFinderManager eventId:@"myPage_onlineCustomerService" attributes:nil];
                     NSString *easemobAccount = model.easemobAccount ?: @"";
                     NSString *name = model.name ?: @"";
                     NSString *headPortrait = model.headPortrait ?: @"";
                    [self enterChatroomWithSessionId:easemobAccount targetName:name targetAvatar:headPortrait];
                }];
            }
            break;
        }
        case 4: {
            if (indexPath.row == 0) {
                // 个人设置
                [IEDataFinderManager eventId:@"myPage_setting" attributes:nil];
                vc = [[IESettingViewController alloc] init];
            } else {
                // 游客模式限制功能
                if ([IECache touristIsLogin] && ![IECache userIsLogin]) {
                    [self visitorPreregistrationInApp];
                    return;
                }
                // 我的收藏
                vc = [[IEMyCollectionViewController alloc] init];
                [IEDataFinderManager eventId:@"myPage_favorites" attributes:nil];
            }
            break;
        }
        default:
            return;
    }
    [self.navigationController pushViewController:vc animated:YES];

对于分组效果,则根据要分组的列表项的下标来判断,之后将分组好的项再存入一个数组,即二维数组,在 numberOfRowsInSection 中判断这个二维数组的数量即可。

这样的做法在第一次实现这个页面的时候确实简单,不用考虑太多,但是后面需求变动引起的改动却极度繁琐。

比如说页面一开始中有 12345 个列表项,现在需要将 3 隐藏变成 1245,那就首先要挨个删除三个数据源数组中的相应数据,然后分别在 cellForRowAtIndexPath 和 didSelectRowAtIndexPath 方法中一点一点修改 ifelse,还要修改分组的代码,过程中还得确保下标准确无误。实际上这样改经常会出错。

这还仅仅是最简单的需求,实际上还会有增加隐藏、改变分组、改变顺序、根据用户角色不同而显示不同的项或不同的顺序等等,这样改动繁琐不说,改几次下来,几乎代码里的每个地方都要是 ifelse ,使得代码的维护变得越来越困难,隐藏个列表项都得调试一上午。可以看到上面的代码已经是惨不忍睹了。

4.重构方案

重点解决的问题是,将每个列表项结构化、模块化,而不是分散到代码的各个角落,这样做也能极大地减少代码中 ifelse 数量,使得代码更灵活且易于维护。

4.1.列表项模块化

为 cellModel 添加构造方法,将每个列表项的属性都封装到 cellModel 中,包括图标、颜色、标题、点击方法名等参数。这样就可以将每个列表项的属性都模块化,而不是分散到代码的各个角落。

// 商脉圈
[[IEMyListCellModel alloc] initWithIconString:@"\U0000e642"
                                    iconColor:Color16Hex(0xFFC069)
                                        title:IEText(@"my_business_cycle")
                                        content:nil
                                    selectorName:@"tapBusinessCycle"]

4.2.构造二维数组

将同一分组的列表项按顺序放到同一数组中组成[a, b] [c, d],再将这些数组按顺序放到一个数组中组成[[a, b], [c, d]],这样就构造出了一个二维数组,这个二维数组就是分组好的列表项。

NSMutableArray *array = [NSMutableArray array];
[array addObject:@[
    // 我的胸卡
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000e63e"
                                        iconColor:Color16Hex(0xFF7875)
                                            title:IEText(@"my_exhibition_card")
                                            content:nil
                                        selectorName:@"tapMyCard"],
    // 为他人预登记
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000E675"
                                        iconColor:Color16Hex(0xFF9C6E)
                                            title:IEText(@"my_pre_registration")
                                            content:nil
                                        selectorName:@"tapRegForOther"],
]];
[array addObject:@[
    // 意向订单管理
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000e6a8"
                                        iconColor:Color16Hex(0x316C2E)
                                            title:IEText(@"my_intention_order")
                                            content:nil
                                        selectorName:@"tapIntentionOrder"],
    // 商脉圈
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000e642"
                                        iconColor:Color16Hex(0xFFC069)
                                            title:IEText(@"my_business_cycle")
                                            content:nil
                                        selectorName:@"tapBusinessCycle"],
]];
[array addObject:@[
    // 在线客服
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000e644"
                                        iconColor:Color16Hex(0x95DE64)
                                            title:IEText(@"my_online_service")
                                            content:nil
                                        selectorName:@"tapOnlineService"],
]];
[array addObject:@[
    // 个人设置
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000e63f"
                                        iconColor:Color16Hex(0x5CDBD3)
                                            title:IEText(@"my_person_setting")
                                            content:nil
                                        selectorName:@"tapSetting"],
    // 我的收藏
    [[IEMyListCellModel alloc] initWithIconString:@"\U0000e61c"
                                        iconColor:Color16Hex(0x69C0FF)
                                            title:IEText(@"my_collection")
                                            content:nil
                                        selectorName:@"tapMyCollection"],
]];

4.3.实现点击事件

根据 cellModel 中的 selectorName 来实现点击事件

- (void)tapIntentionOrder {
    // 意向订单管理
    // 游客模式限制功能
    if ([IECache touristIsLogin] && ![IECache userIsLogin]) {
        [self visitorPreregistrationInApp];
        return;
    }
    [self toOrderManager];
}

4.4.实现 tableView 代理

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.sourceArray.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return ((NSArray *)[self.sourceArray objectAtIndex:section]).count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *listCellId = @"IEMyListCell";
    IEMyListCell *cell = [tableView dequeueReusableCellWithIdentifier:listCellId];
    if (cell == nil) {
        cell = [[IEMyListCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:listCellId];
    }
    cell.cellModel = [[self.sourceArray objectAtIndex:indexPath.section-1] objectAtIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    if (indexPath.section == 0) {
    } else {
        IEMyListCellModel *model = self.sourceArray[indexPath.section-1][indexPath.row];
        if (!kStringIsEmpty(model.selectorName)) {
            // 参考:https://stackoverflow.com/questions/7017281/performselector-may-cause-a-leak-because-its-selector-is-unknown
            SEL action = NSSelectorFromString(model.selectorName);
            if ([self respondsToSelector:action]) {
                ((void (*)(id, SEL))[self methodForSelector:action])(self, action);
            }
        }
    }
}

5.后期维护

  • 增加、删除、隐藏、显示某个列表项,只需修改 sourceArray 中的数据即可。
  • 改变列表项的图标、颜色、标题等,只需修改对应 cellModel 中的数据即可。
  • 改变列表项的点击事件,只需修改对应 cellModel 中 selectorName 保存的方法即可。
  • 根据用户角色、权限等动态改变列表项,只需为不同角色、权限的用户构造不同的 sourceArray 即可。

6.总结

重构后,代码量减少了 50%,代码逻辑更加直观清晰。维护时基本只需要修改数据源数组,不需要修改其他任何地方的代码,大大降低了代码的维护难度,提高效率。
对于项目中其他类似的列表,也可以采用相同的方式进行重构。

7.进一步优化和改进

  • 可以为列表项增加更多的属性,如是否显示右侧箭头、是否显示红点、是否显示分割线等。
  • 使用懒加载的方式,为不同的用户角色直接定义不同的数据源数组
  • 使用分类的方式,将列表项的点击事件和 cellModel 分离开来,使得 cellModel 只负责数据,点击事件只负责点击事件,这样就可以将点击事件的实现放到不同的类中,使得代码更加清晰。
  • 可以将 selectorName 定义为字符串常量,这样就可以在编译时就检查出 selectorName 是否正确,避免运行时出现找不到方法的错误。
  • 可以将 cellModel 定义成一个属性,并在 get 方法中实现 cellModel 的初始化,这样可以使代码更加清晰。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYNajBGS' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片