一种多种类Cell列表的实现

作者: Nagi | 来源:发表于2018-04-04 16:55 被阅读176次

    需求描述

    有一个表格,需要显示不同种类的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];
    }
    

    相关文章

      网友评论

      • 我的珊妮:求Demo
        Nagi:@我的珊妮 关键代码都在文章里了,因为这个和业务有一点耦合,所以没有demo

      本文标题:一种多种类Cell列表的实现

      本文链接:https://www.haomeiwen.com/subject/ulxlhftx.html