一种多种类Cell列表的实现

需求描述

有一个表格,需要显示不同种类的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];
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,760评论 25 707
  • 女孩,别把男朋友当做你世的全部。无论多忙、多爱你的男朋友,也请你不要忘了在你孤单时陪伴你的好朋友。 我有一...
    612d0f1fed06阅读 2,498评论 0 9
  • 1.根本方法:通过事物的定义搞清事物的本质。是什么是一切问题的根源 2.关于正义, 国家的正义: 国家起源...
    breastli阅读 154评论 1 1
  • 孩子考完放假了, 假期第一天清晨, 公园遛孩孩遛猫。 熟悉的场景,先找个方向! 目标明确,开爬! 那棵歪脖树就在那...
    一棵树的生长阅读 393评论 6 6
  • 鸟倦不知归巢, 贪恋斜阳余晖, 一天终尽未留, 黑夜无目乱飞。 解析:我就像那只疲倦不堪的鸟儿,因为贪恋那一点温暖...
    自悟阅读 148评论 0 0