需求描述
有一個(gè)表格,需要顯示不同種類的Cell,種類>10, 隨時(shí)新增新的種類,而且各種類型有相似點(diǎn),分多個(gè)系列,如何設(shè)計(jì)使可維護(hù)性比較高?這里以機(jī)票,火車票,酒店來(lái)舉例。
架構(gòu)選擇
MVC MVVM VIPER
關(guān)于這幾種架構(gòu)不多說(shuō),實(shí)際采用的實(shí)現(xiàn)是基于MVVM,吸收VIPER的優(yōu)點(diǎn),組合成的MVVMIP架構(gòu),Model(Entity), Interactor, UIModel(VM), Presenter, View, 將MVVM中VM的職責(zé)進(jìn)一步細(xì)分。

*Interactor 交互器 負(fù)責(zé)數(shù)據(jù)的獲取Entity,生成UI用的Model
*Presenter 展示器 負(fù)責(zé)View的展示
實(shí)現(xiàn)
常規(guī)實(shí)現(xiàn)
先來(lái)談一談常見(jiàn)可能存在的一種實(shí)現(xiàn)方式:
-
UIModel
機(jī)票、酒店、火車票,對(duì)應(yīng)三種 UIModel,列表顯示時(shí)還包含日期、按鈕操作、信息提示等UI,這些信息將包含在三種 Model 中,通過(guò)參數(shù)來(lái)控制顯示與否。
-
Cell
對(duì)應(yīng)3種 Model 有三種Cell,在創(chuàng)建 TableView 的地方需要注冊(cè)不同Cell的 identifier,在Cell創(chuàng)建的地方,通過(guò)Switch Type 返回不同類型的Cell。
-
事件回調(diào)
每種 Cell 都有事件回調(diào),那么就有3種Deleage, VC 需要實(shí)現(xiàn)這些協(xié)議。
好了,一切貌似比較順利,現(xiàn)在要新加一種打車類型,需要做些什么?
step1 創(chuàng)建一個(gè)新的CellType枚舉類型
step2 新增一種 Model,對(duì)應(yīng)用車,大多數(shù)變量與前三者一致。
step3 創(chuàng)建一種新的Cell
step4 在創(chuàng)建 TableView 的地方需要注冊(cè)新Cell的 identifier
step5 TableView 聲明實(shí)現(xiàn)新的 delegate
step6 在Cell創(chuàng)建的地方,通過(guò)Switch Type 返回新的類型的Cell
在整個(gè)流程過(guò)程中需要重復(fù)做很多工作,會(huì)寫很多類似的代碼,也很難重用;
新的需求是不同渠道創(chuàng)建的機(jī)票、火車票將有另外一種顯示方式,50%與原來(lái)一樣,這個(gè)時(shí)候,相信就有點(diǎn)糾結(jié)了,如果新建新的類型,那么將有50%的代碼和之前一樣,如果追求代碼的重用,擴(kuò)充原來(lái)的類型,那么,不用多說(shuō),整個(gè)結(jié)構(gòu)就越來(lái)越難以維護(hù),無(wú)論是新增,還是修改,都很費(fèi)勁。這樣一來(lái),加班就少不了了。
實(shí)現(xiàn)效果
那么,再來(lái)說(shuō)說(shuō)另外一種實(shí)現(xiàn),最終實(shí)現(xiàn)的效果是,如果想新增一種cell,那么只需要三步:
step1
創(chuàng)建一個(gè)新的CellType枚舉類型
step2
創(chuàng)建對(duì)應(yīng)的UIModel,其type類型設(shè)置為第一步新建的type類型
step3
創(chuàng)建用于顯示的UIView,對(duì),沒(méi)看錯(cuò),是UIView,不是Cell,UIView的內(nèi)容顯示通過(guò) SetUIModel 來(lái)控制。
實(shí)現(xiàn)細(xì)節(jié)
Model
對(duì) Cell 類型進(jìn)行更高層次的抽象:不僅僅機(jī)票、酒店、火車票,將日期、操作、說(shuō)明也抽象成類型,定義BaseModel,通過(guò)繼承的方式,分為數(shù)據(jù)類型 DataModel 和非數(shù)據(jù)類型 SpecialModel 兩種,進(jìn)行定義,通過(guò)多層繼承可進(jìn)一步避免重復(fù)定義變量。
將非數(shù)據(jù)類型也定義為類型的好處是,將這部分 UI 控制邏輯下沉到 Model 創(chuàng)建之處:
網(wǎng)絡(luò)/持久化數(shù)據(jù) Entity -> UIModel,在這個(gè)過(guò)程中,創(chuàng)建額外的非數(shù)據(jù)型UIModel,只要數(shù)據(jù)創(chuàng)建好,后期就不用再理相關(guān)邏輯了。
Cell
定義BaseCell, Cell子類型通過(guò)運(yùn)行時(shí)動(dòng)態(tài)創(chuàng)建,UI顯示通過(guò)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;
}
通過(guò)一系列解耦,將變化分散到兩端:Model 和 View,中間流程全部自動(dòng)化。最上層View,減小粒度,方便組合重用。
View
手法
枚舉與字符串的轉(zhuǎn)化
通過(guò)一系列宏定義,實(shí)現(xiàn)枚舉與字符串的互轉(zhuǎn)
// 枚舉定義展開 1-1
#define ENUM_VALUE(name, assign) name assign,
// 枚舉轉(zhuǎn)字符串case展開 2-1
#define ENUM_CASE(name, assign) case name: return @#name;
// 字符串轉(zhuǎn)枚舉展開 2-1
#define ENUM_STRCMP(name, assign) if ([string isEqualToString:@#name]) return name;
// 枚舉字符串互轉(zhuǎn)函數(shù)展開 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)
// 自動(dòng)生成函數(shù) 枚舉轉(zhuǎn)字符串
//NSString *NSStringFromScheduleType(ScheduleType value);
// 自動(dòng)生成函數(shù) 字符串轉(zhuǎn)枚舉
//EnumType ScheduleTypeFromNSString(NSString *string);
*/
Cell子類自動(dòng)創(chuàng)建
#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);
}];
});
}
// 運(yùn)行時(shí)注冊(cè)子cell重用標(biāo)識(shí)符
+ (void)regSubClassOn:(UITableView*)tableView {
[subCell enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[tableView registerClass:NSClassFromString(obj) forCellReuseIdentifier:obj];
}];
}
View Delegate到Cell的轉(zhuǎn)發(fā)
BaseCell.m
因?yàn)槭荲iew放置在Cell的ContentView上,因此,View的Delegate是Cell,Cell通過(guò)消息轉(zhuǎn)發(fā)實(shí)現(xiàn)回調(diào),避免Cell實(shí)現(xiàn)中手寫回調(diào)中轉(zhuǎn)。
-(void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation invokeWithTarget:self.delegate];
}