需求描述
有一个表格,需要显示不同种类的Cell,种类>10, 随时新增新的种类,而且各种类型有相似点,分多个系列,如何设计使可维护性比较高?这里以机票,火车票,酒店来举例。
架构选择
MVC MVVM VIPER
关于这几种架构不多说,实际采用的实现是基于MVVM,吸收VIPER的优点,组合成的MVVMIP架构,Model(Entity), Interactor, UIModel(VM), Presenter, View, 将MVVM中VM的职责进一步细分。
*Interactor 交互器 负责数据的获取Entity,生成UI用的Model
*Presenter 展示器 负责View的展示
实现
常规实现
先来谈一谈常见可能存在的一种实现方式:
-
UIModel
机票、酒店、火车票,对应三种 UIModel,列表显示时还包含日期、按钮操作、信息提示等UI,这些信息将包含在三种 Model 中,通过参数来控制显示与否。
-
Cell
对应3种 Model 有三种Cell,在创建 TableView 的地方需要注册不同Cell的 identifier,在Cell创建的地方,通过Switch Type 返回不同类型的Cell。
-
事件回调
每种 Cell 都有事件回调,那么就有3种Deleage, VC 需要实现这些协议。
好了,一切貌似比较顺利,现在要新加一种打车类型,需要做些什么?
step1 创建一个新的CellType枚举类型
step2 新增一种 Model,对应用车,大多数变量与前三者一致。
step3 创建一种新的Cell
step4 在创建 TableView 的地方需要注册新Cell的 identifier
step5 TableView 声明实现新的 delegate
step6 在Cell创建的地方,通过Switch Type 返回新的类型的Cell
在整个流程过程中需要重复做很多工作,会写很多类似的代码,也很难重用;
新的需求是不同渠道创建的机票、火车票将有另外一种显示方式,50%与原来一样,这个时候,相信就有点纠结了,如果新建新的类型,那么将有50%的代码和之前一样,如果追求代码的重用,扩充原来的类型,那么,不用多说,整个结构就越来越难以维护,无论是新增,还是修改,都很费劲。这样一来,加班就少不了了。
实现效果
那么,再来说说另外一种实现,最终实现的效果是,如果想新增一种cell,那么只需要三步:
step1
创建一个新的CellType枚举类型
step2
创建对应的UIModel,其type类型设置为第一步新建的type类型
step3
创建用于显示的UIView,对,没看错,是UIView,不是Cell,UIView的内容显示通过 SetUIModel 来控制。
实现细节
Model
对 Cell 类型进行更高层次的抽象:不仅仅机票、酒店、火车票,将日期、操作、说明也抽象成类型,定义BaseModel,通过继承的方式,分为数据类型 DataModel 和非数据类型 SpecialModel 两种,进行定义,通过多层继承可进一步避免重复定义变量。
将非数据类型也定义为类型的好处是,将这部分 UI 控制逻辑下沉到 Model 创建之处:
网络/持久化数据 Entity -> UIModel,在这个过程中,创建额外的非数据型UIModel,只要数据创建好,后期就不用再理相关逻辑了。
Cell
定义BaseCell, Cell子类型通过运行时动态创建,UI显示通过CardBaseView作为容器,加载到Cell 的 ContentView上。
BaseCell.m
- (void)configCellBy:(ScheduleModelBase*)model {
self.model = model;
CardBaseView* card = [self.contentView viewWithTag:tagScheduleView];
card = [ScheduleCardViewMaker makeScheduleCardView:card byModel:model];
if (card.tag != tagScheduleView) {
self.backgroundColor = [UIColor clearColor];
self.contentView.backgroundColor = [UIColor clearColor];
card.tag = tagScheduleView;
card.delegate = self;
[self.contentView addSubview:card];
[card mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.contentView);
make.left.equalTo(self.contentView);
make.bottom.equalTo(self.contentView).priorityLow();
make.right.equalTo(self.contentView).priority(999);
}];
}
}
ScheduleCardViewMaker.m
+ (CardBaseView*)makeScheduleCardView:(CardBaseView*)card
byModel:(ScheduleModelBase*)model {
NSString* typeString = NSStringFromScheduleType(model.type);
NSString* classString = [NSString stringWithFormat:@"Schedule%@CardView", [typeString substringFromIndex:12]];
card = [self p_addCard:NSClassFromString(classString) onCard:card model:model];
return card;
}
+ (CardBaseView*)p_addCard:(Class)class onCard:(CardBaseView*)card model:(ScheduleModelBase*)model {
if (!card) {
card = [class new];
}
[card SetUIModel:model];
return card;
}
通过一系列解耦,将变化分散到两端:Model 和 View,中间流程全部自动化。最上层View,减小粒度,方便组合重用。
View
手法
枚举与字符串的转化
通过一系列宏定义,实现枚举与字符串的互转
// 枚举定义展开 1-1
#define ENUM_VALUE(name, assign) name assign,
// 枚举转字符串case展开 2-1
#define ENUM_CASE(name, assign) case name: return @#name;
// 字符串转枚举展开 2-1
#define ENUM_STRCMP(name, assign) if ([string isEqualToString:@#name]) return name;
// 枚举字符串互转函数展开 2
#define DEFINE_ENUM(EnumType, ENUM_DEF) \
NSString *NSStringFrom##EnumType(EnumType value) \
{ \
switch(value) \
{ \
ENUM_DEF(ENUM_CASE) \
default: return @""; \
} \
} \
EnumType EnumType##FromNSString(NSString *string) \
{ \
ENUM_DEF(ENUM_STRCMP) \
return (EnumType)0; \
}
// 枚举声明定义宏
#define DECLARE_ENUM(EnumType, ENUM_DEF) \
typedef NS_ENUM(NSInteger, EnumType) { \
ENUM_DEF(ENUM_VALUE) \
}; \
NSString *NSStringFrom##EnumType(EnumType value); \
EnumType EnumType##FromNSString(NSString *string); \
/*example
// step 1 .h 行程卡片类型枚举
#define SCHEDULE_TYPE(__x) \
__x(ScheduleTypeFlight, ) \
__x(ScheduleTypeSpecial, ) \
__x(ScheduleTypeSpecialTime, ) \
__x(ScheduleTypeCount, ) \
// step 2 .h 声明
DECLARE_ENUM(ScheduleType, SCHEDULE_TYPE)
// step 3 .m
DEFINE_ENUM(ScheduleType, SCHEDULE_TYPE)
// 自动生成函数 枚举转字符串
//NSString *NSStringFromScheduleType(ScheduleType value);
// 自动生成函数 字符串转枚举
//EnumType ScheduleTypeFromNSString(NSString *string);
*/
Cell子类自动创建
#define RegTableCellClass(cellName) \
Class clazz##cellName = objc_allocateClassPair(self, cellName.UTF8String, 0); \
objc_registerClassPair(clazz##cellName);
#define ENUM_TO_CSTR_CASE(enumType) \
[NSString stringWithCString:#enumType encoding:NSASCIIStringEncoding]
BaseCell.m
static NSMutableArray* subCell = nil;
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
subCell = [NSMutableArray new];
for (int type = 0; type < ScheduleTypeCount; ++type) {
NSString* cellString = [NSString stringWithFormat:@"%@Cell", NSStringFromScheduleType(type)];
[subCell addObject:cellString];
}
[subCell enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
RegTableCellClass(obj);
}];
});
}
// 运行时注册子cell重用标识符
+ (void)regSubClassOn:(UITableView*)tableView {
[subCell enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[tableView registerClass:NSClassFromString(obj) forCellReuseIdentifier:obj];
}];
}
View Delegate到Cell的转发
BaseCell.m
因为是View放置在Cell的ContentView上,因此,View的Delegate是Cell,Cell通过消息转发实现回调,避免Cell实现中手写回调中转。
-(void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:self.delegate];
}