一種多種類Cell列表的實(shí)現(xiàn)

需求描述

有一個(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ì)分。


架構(gòu)示意圖

*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];
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,094評(píng)論 25 709
  • 女孩,別把男朋友當(dāng)做你世的全部。無(wú)論多忙、多愛(ài)你的男朋友,也請(qǐng)你不要忘了在你孤單時(shí)陪伴你的好朋友。 我有一...
    612d0f1fed06閱讀 2,865評(píng)論 0 9
  • 1.根本方法:通過(guò)事物的定義搞清事物的本質(zhì)。是什么是一切問(wèn)題的根源 2.關(guān)于正義, 國(guó)家的正義: 國(guó)家起源...
    breastli閱讀 187評(píng)論 1 1
  • 孩子考完放假了, 假期第一天清晨, 公園遛孩孩遛貓。 熟悉的場(chǎng)景,先找個(gè)方向! 目標(biāo)明確,開爬! 那棵歪脖樹就在那...
    一棵樹的生長(zhǎng)閱讀 448評(píng)論 6 6
  • 鳥倦不知?dú)w巢, 貪戀斜陽(yáng)余暉, 一天終盡未留, 黑夜無(wú)目亂飛。 解析:我就像那只疲倦不堪的鳥兒,因?yàn)樨潙倌且稽c(diǎn)溫暖...
    自悟閱讀 198評(píng)論 0 0

友情鏈接更多精彩內(nèi)容