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ā)
donggelaile:一站式搭建各種滑動(dòng)列表(Objective-C)
基于MVVM,用于快速搭建設(shè)置頁领曼,個(gè)人信息頁的框架
利用MVVM設(shè)計(jì)快速開發(fā)個(gè)人中心鸥鹉、設(shè)置等模塊