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 的初始化,这样能够使代码愈加明晰。