UITableView 建模 -- 提高代碼復(fù)用

tableview 是開發(fā)中項(xiàng)目中常用的視圖控件甫匹,并且是重復(fù)的使用,布局類似夹界,只是數(shù)據(jù)源及Cell更改飘千,所以會(huì)出現(xiàn)很多重復(fù)的內(nèi)容,并且即使新建一個(gè)基礎(chǔ)的列表也要重復(fù)這些固定邏輯的代碼伞芹,這對(duì)于開發(fā)效率很不友好忘苛。
本文的重點(diǎn)是抽取重復(fù)的邏輯代碼簡(jiǎn)化列表頁面的搭建唱较,達(dá)到數(shù)據(jù)驅(qū)動(dòng)列表

思路草稿

說明:
首先tableview有兩個(gè)代理delegate 和 datasource(基于單一職責(zé)設(shè)計(jì)規(guī)則)
delegate :負(fù)責(zé)交互事件扎唾;
datasource :負(fù)責(zé)cell創(chuàng)建及數(shù)據(jù)填充,這也是本文探討的重點(diǎn)南缓。
(1)基本原則
蘋果將tableView的數(shù)據(jù)通過一個(gè)二維數(shù)組構(gòu)建(組胸遇,行),這是一個(gè)很重要的設(shè)計(jì)點(diǎn)汉形,要沿著這套規(guī)則繼續(xù)發(fā)展纸镊,設(shè)計(jì)模式的繼承,才是避免壞代碼產(chǎn)生的基礎(chǔ)获雕。
(2)組
“組”是這套邏輯的根基薄腻,先有組再有行,并且列表動(dòng)態(tài)修改的內(nèi)容都是以為基礎(chǔ)届案,的結(jié)構(gòu)相對(duì)固定庵楷,因此本文將抽離成一個(gè)數(shù)據(jù)模型而不是接口楣颠。

#import <Foundation/Foundation.h>
#import "RWCellViewModelProtocol.h"

@interface RWSectionModel : NSObject
/// item數(shù)組:元素必須是遵守RWCellViewModel協(xié)議
@property (nonatomic, strong) NSMutableArray <id<RWCellViewModel>>*itemsArray;

/// section頭部高度
@property (nonatomic, assign) CGFloat  sectionHeaderHeight;
/// section尾部高度
@property (nonatomic, assign) CGFloat  sectionFooterHeight;
/// sectionHeaderView: 必須是UITableViewHeaderFooterView或其子類尽纽,并且遵循RWHeaderFooterDataSource協(xié)議
@property (nonatomic, strong) Class headerReuseClass;
/// sectionFooterView: 必須是UITableViewHeaderFooterView或其子類,并且遵循RWHeaderFooterDataSource協(xié)議
@property (nonatomic, strong) Class footerReuseClass;

/// headerData
@property (nonatomic, strong) id headerData;
/// footerData
@property (nonatomic, strong) id footerData;
@end

(2)行
最核心的有三大Cell童漩、Cell高度弄贿、Cell數(shù)據(jù)
這次的設(shè)計(jì)參考MVVM設(shè)計(jì)模式矫膨,對(duì)于行的要素提取成一個(gè)ViewModel差凹,并且ViewModel要做成接口的方式期奔,因?yàn)樾谐诉@三個(gè)基本的元素外,可能要需要Cell填充的數(shù)據(jù)危尿,比如titleString呐萌,subTitleString,headerImage等等谊娇,這樣便于擴(kuò)展肺孤。

#ifndef RWCellViewModel_h
#define RWCellViewModel_h

@import UIKit;

@protocol RWCellViewModel <NSObject>
/// Cell 的類型
@property (nonatomic, strong) Class cellClass;
/// Cell的高度:  0 則是UITableViewAutomaticDimension
@property (nonatomic, assign) CGFloat  cellHeight;
@end

#endif /* RWCellViewModel_h */

(3)tableView
此處不用使用tableViewController的方式,而使用view的方式济欢,這樣嵌入更方便赠堵。并且對(duì)外提供基本的接口,用于列表數(shù)據(jù)的獲取法褥,及點(diǎn)擊事件處理茫叭。

備注:
關(guān)于數(shù)據(jù),這里提供了多組和單組的兩個(gè)接口挖胃,為了減少使用的過程中外部新建RWSectionModel這一步杂靶,但是其內(nèi)部還是基于RWSectionModel這一個(gè)模型。

#import <UIKit/UIKit.h>
#import "RWCellViewModelProtocol.h"
#import "RWSectionModel.h"

@protocol RWTableViewDelegate;

@interface RWTableView : UITableView
/// rwdelegate
@property (nonatomic, weak) id<RWTableViewDelegate> rwdelegate;

/// 構(gòu)建方法
/// @param delegate 是指rwdelegate
- (instancetype)initWithDelegate:(id<RWTableViewDelegate>)delegate;

@end


@protocol RWTableViewDelegate <NSObject>
@optional
/// 多組構(gòu)建數(shù)據(jù)
- (NSArray <RWSectionModel*>*)tableViewWithMutilSectionDataArray;

/// 單組構(gòu)建數(shù)據(jù)
- (NSArray <id<RWCellViewModel>>*)tableViewWithSigleSectionDataArray;


/// cell點(diǎn)擊事件
/// @param data cell數(shù)據(jù)模型
/// @param indexPath indexPath
- (void)tableViewDidSelectedCellWithDataModel:(id)data indexPath:(NSIndexPath *)indexPath;
RWTableview.m

#pragma mark - dataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    /// 數(shù)據(jù)源始終保持“二維數(shù)組的狀態(tài)”酱鸭,即SectionModel中包裹items的方式
    if ([self.rwdelegate respondsToSelector:@selector(tableViewWithMutilSectionDataArray)]) {
        self.dataArray = [self.rwdelegate tableViewWithMutilSectionDataArray];
        return self.dataArray.count;
    }
    else if ([self.rwdelegate respondsToSelector:@selector(tableViewWithSigleSectionDataArray)]) {
        RWSectionModel *sectionModel = [[RWSectionModel alloc]init];
        sectionModel.itemsArray = [self.rwdelegate tableViewWithSigleSectionDataArray].mutableCopy;
        self.dataArray = @[sectionModel];
        return 1;
    }
    return 0;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:section];
    return sectionModel.itemsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    /// 此處只做Cell的復(fù)用或創(chuàng)建
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    if (cell == nil) {
        cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    }
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}

(4)Cell上子控件的交互事件處理
騰訊QQ部門的大神峰之巔提供了一個(gè)很好的解決辦法吗垮,基于蘋果現(xiàn)有的響應(yīng)鏈(真的很牛逼),將點(diǎn)擊事件傳遞給下個(gè)響應(yīng)者凹髓,而不需要為事件的傳遞搭建更多的依賴關(guān)系烁登。這是一篇雞湯文章,有很多營(yíng)養(yǎng)蔚舀,比如tableview模塊化饵沧,這也是我接下來要學(xué)習(xí)的。

#import <UIKit/UIKit.h>
#import "RWEvent.h"

@interface UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event;

@end
#import "UIResponder+RWEvent.h"

@implementation UIResponder (RWEvent)

- (void)respondEvent:(NSObject<RWEvent> *)event {
    [self.nextResponder respondEvent:event];
}

@end

2020年11月18日 更新

鑒于此tableView封裝在實(shí)際項(xiàng)目遇到的問題進(jìn)行改善赌躺,主要內(nèi)容如下:
(1)使用分類的方式替換協(xié)議
優(yōu)點(diǎn):分類能更便捷的擴(kuò)展原有類狼牺,并且使用更方便,不需要再導(dǎo)入?yún)f(xié)議文件及遵守協(xié)議
【RWCellDataSource協(xié)議】替換成:【UITableViewCell (RWData)】
【RWHeaderFooterDataSource協(xié)議】 替換成:【UITableViewHeaderFooterView (RWData)】

(2)cell高度緩存的勘誤
willDisplayCell:中要想獲取準(zhǔn)確的Cell高度礼患,那么必須在heightForRowAtIndexPath:方法中給Cell賦值是钥,因?yàn)橄到y(tǒng)計(jì)算Cell的高度是在這個(gè)方法中進(jìn)行的

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    /// Cell創(chuàng)建
    if (cell == nil) {
        cell = [[cellViewModel.cellClass alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NSStringFromClass(cellViewModel.cellClass)];
    }
    /// Cell賦值
    [cell rw_setData:cellViewModel];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    return cellViewModel.cellHeight ? : UITableViewAutomaticDimension;
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    RWSectionModel *sectionModel = [self.dataArray objectAtIndex:indexPath.section];
    id<RWCellViewModel>cellViewModel = [sectionModel.itemsArray objectAtIndex:indexPath.row];
    /// 高度緩存
    /// 此處高度做一個(gè)緩存是為了高度自適應(yīng)的Cell,避免重復(fù)計(jì)算的工作量缅叠,對(duì)于性能優(yōu)化有些幫助
    /// 如果想要在willDisplayCell獲取到準(zhǔn)確的Cell高度悄泥,那么必須在cellForRowAtIndexPath:方法給Cell賦值
    /// 同時(shí)可以避免由于高度自適應(yīng)導(dǎo)致Cell的定位不準(zhǔn)確,比如置頂或者滑動(dòng)到某一個(gè)Cell的位置
    /// 如果自動(dòng)布局要更新高度肤粱,可以將cellViewModel設(shè)置為0
    cellViewModel.cellHeight = cell.frame.size.height;
}

完整代碼

特別感謝以下作者寫的文章弹囚,給我很多啟發(fā)

峰之巔:iOS 高效開發(fā)解決方案

donggelaile:一站式搭建各種滑動(dòng)列表(Objective-C)

基于MVVM,用于快速搭建設(shè)置頁领曼,個(gè)人信息頁的框架

利用MVVM設(shè)計(jì)快速開發(fā)個(gè)人中心鸥鹉、設(shè)置等模塊

如何優(yōu)雅的插入廣告

iOS面向切面的TableView-AOPTableView

iOS面向切面的TableView-AOPTableView

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末蛮穿,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宋舷,更是在濱河造成了極大的恐慌绪撵,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件祝蝠,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡幻碱,警方通過查閱死者的電腦和手機(jī)绎狭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褥傍,“玉大人儡嘶,你說我怎么就攤上這事』蟹纾” “怎么了蹦狂?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)朋贬。 經(jīng)常有香客問我凯楔,道長(zhǎng),這世上最難降的妖魔是什么锦募? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任摆屯,我火速辦了婚禮,結(jié)果婚禮上糠亩,老公的妹妹穿的比我還像新娘虐骑。我一直安慰自己,他們只是感情好赎线,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布廷没。 她就那樣靜靜地躺著,像睡著了一般垂寥。 火紅的嫁衣襯著肌膚如雪颠黎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天矫废,我揣著相機(jī)與錄音盏缤,去河邊找鬼。 笑死蓖扑,一個(gè)胖子當(dāng)著我的面吹牛唉铜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播律杠,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼潭流,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼竞惋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起灰嫉,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤拆宛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后讼撒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體浑厚,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年根盒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了钳幅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炎滞,死狀恐怖敢艰,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情册赛,我是刑警寧澤钠导,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站森瘪,受9級(jí)特大地震影響牡属,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜柜砾,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一湃望、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧痰驱,春花似錦证芭、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蝇完,卻和暖如春官硝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背短蜕。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工氢架, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人朋魔。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓岖研,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子孙援,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容