玩转UITableView
UITableView这个iOS开发中永远绕不开的UIView,那么就不可避免的要在多个页面多种场景下反复摩擦UITableView,就算是刚跳进火坑不久的iOS Developer也知道实现UITableView的数据源dataSource和代理delegate,写出一个UITableView也就基本OK了,但是这仅仅是写出一个UITableView而已,作为一个有想法的程序猿,要做的还有很多,如何利用UITableViewCell的重用机制,如何提高性能等,这些留在后面的系列中一一讲述,那么本文要解决的痛点又是什么呢?回答这个问题之前,我们先来看看上面提到的UITableView的两大核心:UITableViewDataSource、UITableViewDelegate!
一、UITableViewDataSource
UITableView需要一个数据源(dataSource)来显示数据,UITableView会向数据源查询一共有多少行数据以及每一行显示什么数据等。没有设置数据源的UITableView只是个空壳。凡是遵守UITableViewDataSource协议的OC对象,都可以是UITableView的数据源。查看源码:
@required // 必须实现 // 每个section的行数 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section; // 第section分区第row行的UITableViewCell对象 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; @optional // 可选实现 // section个数,默认是1 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView; // 第section分区的头部标题 - (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section; // 第section分区的底部标题 - (nullable NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section; // 某一行是否可以编辑(删除) - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath; // 某一行是否可以移动来进行重新排序 - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath; // UITableView右边的索引栏的内容 // return list of section titles to display in section index view (e.g. "ABCD...Z#") - (nullable NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView;
@protocol UITableViewDelegate<NSObject, UIScrollViewDelegate> @optional // 每行高度 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; // 每个section头部高度 - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section; // 每个section底部高度 - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section; // 每个section头部自定义UIView - (nullable UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section; // 每个section底部自定义UIView - (nullable UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section; // 是否允许高亮 - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0); // 选中某行 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
到这里已经很明确了,在需要实现UITableView的控制器对象里,就不可避免的要设置数据源和设置代理,那么就不可避免的需要实现以上提到的那些代理方法,试想一下,如果不进行有效的封装,那极有可能每个需要UITableView的Controller里都有如下重复的代码行:
#pragma mark - UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { return 0.000001; } - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{ return 0.000001; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ return 0; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ return 0.000001; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ return 0; } - (UIView*)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{ return nil; } - (UIView*)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{ return nil; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"defaultType"]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ return; } // lazy load - (UITableView*)tableView{ if (!_tableView) { _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, -64, KS_Width, KS_Heigth+64) style:UITableViewStyleGrouped]; _tableView.delegate = (id)self; _tableView.dataSource = (id)self; [_tableView setSectionHeaderHeight:0]; _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; _tableView.showsVerticalScrollIndicator = NO; _tableView.showsHorizontalScrollIndicator = NO; } return _tableView; }
这已经是够灾难的了,如果在项目周期中再遇到某个或者多个页面设计UI设计频繁的变动,那简直不敢想象,哪怕每次只是一点小小的改动,也可能需要修改上面重复代码块中UITableViewDelegate的多个地方,如新插入一行row或者一个section,所有涉及到section或者row的地方或许都需要更改!!!
OK,我现在可以回答上面的问题了,这边文章到底是做什么的?解决的痛点在那里?--- 解耦封装、简化代码、适者生存!
从重复代码块我们可以看出,一般会让控制器充当UITableView的dataSource和delegate,那么既然要解耦,那么就要打破思维定式,让UITableView自己做自己的dataSource和delegate!毕竟我的地盘我做主嘛!其次将UITableViewCell进行block封装对象化,让其所有的属性都自我集成。
一、首先来看UITableViewCell的封装 -- ZTCoolTableViewCell
@class UIView; @class UITableViewCell; @class UITableView; @class NSIndexPath; // 创建section头部 Or section底部的block typedef UIView *(^buildCell)(UITableView *tableView, NSInteger section); // 创建section对应的row数据源的block typedef UITableViewCell *(^buildCellInfo)(UITableView *tableView, NSIndexPath *indexPath); // 点击section对应row的事件block typedef void (^clickBlock)(UITableView *tableView, NSIndexPath *indexPath); // ZTCoolTableCellList刷新block typedef void (^refreshBlock)(); @interface ZTCoolTableViewCell : NSObject // 行高度 @property (nonatomic,assign) CGFloat height; // 构造行 @property (nonatomic, copy) buildCell buildCell; @end @interface ZTCoolTableCellList : NSObject // 头部 @property (nonatomic,strong) ZTCoolTableViewCell * headCell; // 底部 @property (nonatomic,strong) ZTCoolTableViewCell * footCell; // 构造行 @property (nonatomic,copy) buildCellInfo buildCellInfo; // 列高(等于0表示自适应) @property (nonatomic,assign) CGFloat cellHeigth; // 行数量 @property (nonatomic,assign) NSInteger cellCount; // 行点击事件 @property(nonatomic,copy) clickBlock clickBlock; // 刷新事件(适用于需要动态更新tableview布局:新增或者删减section/row) @property(nonatomic,copy) refreshBlock refreshBlock; // 行标识 @property (nonatomic,copy) NSString *identifier; @property (nonatomic,copy) NSString *xibName; // 简单初始化 (单行cell) - (ZTCoolTableCellList *)initSimpleCell:(CGFloat)cellHeight buildCell:(buildCellInfo)buildCell clickCell:(clickBlock)clickCell; // 复杂初始化 - 不可刷新 - (ZTCoolTableCellList *)initComplexCellNoRefresh:(CGFloat)headHeigth buildHead:(buildCell)buildHead footHeight:(CGFloat)footHeight buildFoot:(buildCell)buildFoot cellHeight:(CGFloat)cellHeight buildCell:(buildCellInfo)buildCell clickCell:(clickBlock)clickCell cellCount:(NSInteger)cellCount identifier:(NSString *)identifier xibName:(NSString *)xibName; // 复杂初始化 - 可刷新 - (ZTCoolTableCellList *)initComplexCellHasRefresh:(CGFloat)headHeigth buildHead:(buildCell)buildHead footHeight:(CGFloat)footHeight buildFoot:(buildCell)buildFoot cellHeight:(CGFloat)cellHeight buildCell:(buildCellInfo)buildCell clickCell:(clickBlock)clickCell refreshCell:(refreshBlock)refreshCell cellCount:(NSInteger)cellCount identifier:(NSString *)identifier xibName:(NSString *)xibName; @end
@implementation ZTCoolTableViewCell @end @implementation ZTCoolTableCellList // 简单初始化 - (ZTCoolTableCellList *)initSimpleCell:(CGFloat)cellHeight buildCell:(buildCellInfo)buildCell clickCell:(clickBlock)clickCell{ return [self initComplexCellNoRefresh:0 buildHead:nil footHeight:0 buildFoot:nil cellHeight:cellHeight buildCell:buildCell clickCell:clickCell cellCount:1 identifier:nil xibName:nil]; } // 复杂初始化 - 不可刷新 - (ZTCoolTableCellList *)initComplexCellNoRefresh:(CGFloat)headHeigth buildHead:(buildCell)buildHead footHeight:(CGFloat)footHeight buildFoot:(buildCell)buildFoot cellHeight:(CGFloat)cellHeight buildCell:(buildCellInfo)buildCell clickCell:(clickBlock)clickCell cellCount:(NSInteger)cellCount identifier:(NSString *)identifier xibName:(NSString *)xibName{ if(headHeigth >0){ self.headCell = [[ZTCoolTableViewCell alloc] init]; self.headCell.height = headHeigth; self.headCell.buildCell = buildHead; } if(footHeight >0){ self.footCell = [[ZTCoolTableViewCell alloc] init]; self.footCell.height = footHeight; self.footCell.buildCell = buildFoot; } self.cellHeigth = cellHeight; self.buildCellInfo = buildCell; self.clickBlock = clickCell; self.cellCount = cellCount; self.identifier = identifier; self.xibName = xibName; return self; } // 复杂初始化 - 可刷新 - (ZTCoolTableCellList *)initComplexCellHasRefresh:(CGFloat)headHeigth buildHead:(buildCell)buildHead footHeight:(CGFloat)footHeight buildFoot:(buildCell)buildFoot cellHeight:(CGFloat)cellHeight buildCell:(buildCellInfo)buildCell clickCell:(clickBlock)clickCell refreshCell:(refreshBlock)refreshCell cellCount:(NSInteger)cellCount identifier:(NSString *)identifier xibName:(NSString *)xibName{ if(headHeigth >0){ self.headCell = [[ZTCoolTableViewCell alloc] init]; self.headCell.height = headHeigth; self.headCell.buildCell = buildHead; } if(footHeight >0){ self.footCell = [[ZTCoolTableViewCell alloc] init]; self.footCell.height = footHeight; self.footCell.buildCell = buildFoot; } self.cellHeigth = cellHeight; self.buildCellInfo = buildCell; self.clickBlock = clickCell; if(refreshCell){ self.refreshBlock = refreshCell; } self.cellCount = cellCount; self.identifier = identifier; self.xibName = xibName; return self; }
二、让UITableView自己做自己的dataSource和delegate -- ZTCoolTableViewBase
@class ZTCoolTableCellList; @interface ZTCoolTableViewBase : UITableView <UITableViewDataSource, UITableViewDelegate> // UITableView的数据集合 @property (nonatomic,strong) NSMutableArray<ZTCoolTableCellList*> *arrayTableViewCellList; @end
@implementation ZTCoolTableViewBase #pragma mark-hitTest - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{ id view = [super hitTest:point withEvent:event]; if(![view isKindOfClass:[UITextField class]]){ [self endEditing:YES]; } return view; } #pragma mark - TableViewDelegate // section头部高度 - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section { ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section]; if(cellList.headCell){ return cellList.headCell.height; }else{ return 0.00001; } } // section底部高度 - (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section]; if(cellList.footCell){ return cellList.footCell.height; }else{ return 0.00001; } } // 有多少section - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{ return [self.arrayTableViewCellList count]; } // 改变行的高度 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:[indexPath section]]; if(cellList.cellHeigth == 0){ UITableViewCell *cell = [self tableView:self cellForRowAtIndexPath:indexPath]; return cell.frame.size.height; }else{ return cellList.cellHeigth; } } // 每个section有多少行 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section]; return cellList.cellCount; } // 头部 - (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section]; if(cellList.headCell.buildCell){ return cellList.headCell.buildCell(tableView,section); }else{ return nil; } } // cell数据构造 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:[indexPath section]]; return cellList.buildCellInfo(tableView,indexPath); } // 底部 - (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:section]; if(cellList.footCell.buildCell){ return cellList.footCell.buildCell(tableView,section); }else{ return nil; } } // 选中某个项 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ ZTCoolTableCellList *cellList = [self.arrayTableViewCellList objectAtIndex:[indexPath section]]; if(cellList.clickBlock){ return cellList.clickBlock(tableView,indexPath); } } - (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0){ return YES; }
如此,我们便实现了UITableViewCell的对象化封装和Controller于UITableView数据源及代理的耦合。
那么如何实际运用呢?我们来举个例子,如下图,实现这样一个页面:
按照以前的思维,将控制器充当UITableView的dataSource和delegate,那么就会出现
_tableView.delegate = (id)self;
_tableView.dataSource = (id)self;
而且每个Controller页面都是实现的协议代理方法,一长串的重复代码!!!
那么现在有了新需求,需要动态的再第一个section和第二个section之间新增一个section,包括两行row,这就需要重新代码布局,涉及到了所有 row点击事件极有可能需要重新绑定section与row值,对于能躺着绝对不站着的懒程序猿来说,这简直不要太扎心!如果使用上面封装的设计去实现,简直不要太舒服!
一、声明对象
// 主界面容器UITableView @property (nonatomic,strong) ZTCoolTableViewBase *tableView; // 第一个section(个人资料、我的钱包) @property (nonatomic,strong) ZTCoolTableCellList *firstCell; // 第二个section(交易记录、联系客服、设置) @property (nonatomic,strong) ZTCoolTableCellList *secondCell; // 第三个section(私人日记、统计面板) @property (nonatomic,strong) ZTCoolTableCellList *thirdCell;
二、设置UITableView数据源和代理
- (ZTCoolTableViewBase *)tableView{ if (!_tableView) { CGRect rect = [UIScreen mainScreen].bounds; _tableView = [[ZTCoolTableViewBase alloc] initWithFrame:rect style:UITableViewStyleGrouped]; _tableView.arrayTableViewCellList = [[NSMutableArray alloc] initWithObjects: self.firstCell, self.thirdCell, nil]; _tableView.delegate = _tableView; _tableView.dataSource = _tableView; _tableView.sectionHeaderHeight = 0; _tableView.separatorColor = [UIColor groupTableViewBackgroundColor]; } return _tableView; }
其中:
// 设置UITableView的代理为自己 _tableView.delegate = _tableView; // 设置UITableView的数据源为自己 _tableView.dataSource = _tableView; // 初始化UITableView的数据对象集合 _tableView.arrayTableViewCellList = [[NSMutableArray alloc] initWithObjects: self.firstCell, self.thirdCell, nil];
三、懒加载数据集合
#pragma mark - firstCell - (ZTCoolTableCellList *)firstCell{ if (!_firstCell) { BIWeakObj(self) static NSString *identifier = @"firstCell"; _firstCell = [[ZTCoolTableCellList alloc] init]; _firstCell = [_firstCell initComplexCellNoRefresh:0 buildHead:nil footHeight:10 buildFoot:nil cellHeight:44 buildCell:^UITableViewCell *(UITableView *tableView, NSIndexPath *indexPath) { UITableViewCell *cell = [selfWeak.tableView dequeueReusableCellWithIdentifier:identifier]; if(cell == nil){ cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.textLabel.font = [UIFont systemFontOfSize:14.0f]; if(indexPath.row == 0){ cell.imageView.image = [UIImage imageNamed:@"ic_my_info"]; cell.textLabel.text = @"个人资料"; } else{ cell.imageView.image = [UIImage imageNamed:@"ic_my_money"]; cell.textLabel.text = @"我的钱包"; } } return cell; } clickCell:^(UITableView *tableView, NSIndexPath *indexPath) { [selfWeak clickCell:indexPath]; } cellCount:2 identifier:identifier xibName:nil]; } return _firstCell; }
#pragma mark - secondCell - (ZTCoolTableCellList *)secondCell{ if (!_secondCell) { BIWeakObj(self) static NSString *identifier = @"secondCell"; _secondCell = [[ZTCoolTableCellList alloc] init]; _secondCell = [_secondCell initComplexCellNoRefresh:0 buildHead:nil footHeight:10 buildFoot:nil cellHeight:44 buildCell:^UITableViewCell *(UITableView *tableView, NSIndexPath *indexPath) { UITableViewCell *cell = [selfWeak.tableView dequeueReusableCellWithIdentifier:identifier]; if(cell == nil){ cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.textLabel.font = [UIFont systemFontOfSize:14.0f]; if(indexPath.row == 0){ cell.imageView.image = [UIImage imageNamed:@"ic_my_log"]; cell.textLabel.text = @"私人日记"; } else{ cell.imageView.image = [UIImage imageNamed:@"ic_my_statistic"]; cell.textLabel.text = @"统计面板"; } } return cell; } clickCell:^(UITableView *tableView, NSIndexPath *indexPath) { [selfWeak clickCell:indexPath]; } cellCount:2 identifier:identifier xibName:nil]; } return _secondCell; }
#pragma mark - thirdCell - (ZTCoolTableCellList *)thirdCell{ if (!_thirdCell) { BIWeakObj(self) static NSString *identifier = @"thirdCell"; _thirdCell = [[ZTCoolTableCellList alloc] init]; _thirdCell = [_thirdCell initComplexCellHasRefresh:0 buildHead:nil footHeight:0 buildFoot:nil cellHeight:44 buildCell:^UITableViewCell *(UITableView *tableView, NSIndexPath *indexPath) { UITableViewCell *cell = [selfWeak.tableView dequeueReusableCellWithIdentifier:identifier]; if(cell == nil){ cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:identifier]; cell.selectionStyle = UITableViewCellSelectionStyleNone; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; cell.textLabel.font = [UIFont systemFontOfSize:14.0f]; if(indexPath.row == 0){ cell.imageView.image = [UIImage imageNamed:@"ic_my_quotebill"]; cell.textLabel.text = @"交易记录"; } else if(indexPath.row == 1){ cell.imageView.image = [UIImage imageNamed:@"ic_my_service"]; cell.textLabel.text = @"联系客服"; } else{ cell.imageView.image = [UIImage imageNamed:@"ic_my_setup"]; cell.textLabel.text = @"设置"; } } return cell; } clickCell:^(UITableView *tableView, NSIndexPath *indexPath) { [selfWeak clickCell:indexPath]; } refreshCell:^{ [selfWeak.tableView.arrayTableViewCellList insertObject:selfWeak.secondCell atIndex:1]; [selfWeak.tableView reloadData]; } cellCount:3 identifier:identifier xibName:nil]; } return _thirdCell; }
其中第三个cell可刷新(为了给第二个cell指定新增时的入口)这里是个block:
refreshCell:^{ [selfWeak.tableView.arrayTableViewCellList insertObject:selfWeak.secondCell atIndex:1]; [selfWeak.tableView reloadData]; }
新增按钮点击事件:
- (void)addTableviewSection:(id)sender{ if(self.thirdCell.refreshBlock){ self.thirdCell.refreshBlock(); } }
如此实现,在解耦的同时还能简化重复代码量,并且可以最小的代价cost适应频繁变化的UI设计!
PS:目前的封装只支持每个section块的每行row高度是一样的,如果存在不一致的需求,可在我的基础上进行二次封装变化,如果我的文章对您有些许帮助,帮忙点赞标星,如需转载,请说明出处,谢谢!
demo Github地址:https://github.com/BeckWang0912/ZTCoolTableView 喜欢就标个星星吧✨✨~~~✨✨^o^