
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 的初始化,这样可以使代码更加清晰。