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

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] objectAtIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    IEMyListCellModel *model = self.sourceArray[indexPath.section][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 的初始化,这样能够使代码愈加明晰。