分享

iOS中实现UITableView循环利用

 叹落花 2015-08-20
前言

来源:http://www.jianshu.com/p/bc0a55e9b09b#

作者:啊崢


大家都知道UITableView,最经典在于循环利用,这里我自己模仿UITableView循环利用,写了一套自己的TableView实现方案,希望大家看了我的文章,循环利用思想有显著提升。

效果如图:


研究UITableView底层实现


  1. 系统UITabelView的简单使用,这里就不考虑分组了,默认为1组。


// 返回第section组有多少行

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

{

NSLog(@"%s",__func__);

return 10;

}

// 返回每一行cell的样子

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

NSLog(@"%s",__func__);

static NSString *ID = @"cell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

if (cell == nil) {

cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];

}

cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];

return cell;

}

// 返回每行cell的高度

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath

{

NSLog(@"%s--%@",__func__,indexPath);

return 100;

}


2.验证UITabelView的实现机制。


如图打印结果:



· 分析:底层先获取有多少cell(10个),在获取每个cell的高度,返回高度的方法一开始调用10次。

· 目的:确定tableView的滚动范围,一开始计算所有cell的frame,就能计算下tableView的滚动范围。


· 分析:tableView:cellForRowAtIndexPath:方法什么时候调用。
打印验证,如图:


一开始调用了7次,因为一开始屏幕最多显示7个cell
目的:一开始只加载显示出来的cell,等有新的cell出现的时候会继续调用这个方法加载cell。


3.UITableView循环利用思想


当新的cell出现的时候,首先从缓存池中获取,如果没有获取到,就自己创建cell。
当有cell移除屏幕的时候,把cell放到缓存池中去。

二、自定义UIScroolView


模仿UITableView循环利用


1. 提供数据源和代理方法,命名和UITableView一致。


@class YZTableView;

@protocol YZTableViewDataSource<NSObject>

@required

// 返回有多少行cell

- (NSInteger)tableView:(YZTableView *)tableView numberOfRowsInSection:(NSInteger)section;

// 返回每行cell长什么样子

- (UITableViewCell *)tableView:(YZTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@end

@protocol YZTableViewDelegate<NSObject, UIScrollViewDelegate>

// 返回每行cell有多高

- (CGFloat)tableView:(YZTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

@end


2.提供代理和数据源属性


@interface YZTableView : UIScrollView

@property (nonatomic, weak) id<YZTableViewDataSource> dataSource;

@property (nonatomic, weak) id<YZTableViewDelegate> delegate;

@end


警告:


解决,在YZTableView.m的实现中声明。


· 原因:有人会问为什么我要定义同名的delegate属性,我主要想模仿系统的tableView,系统tableView也有同名的属性。


· 思路:这样做,外界在使用设置我的tableView的delegate,就必须遵守的我的代理协议,而不是UIScrollView的代理协议。


3.提供刷新方法reloadData,因为tableView通过这个刷新tableView。


@interface YZTableView : UIScrollView

@property (nonatomic, weak) id<YZTableViewDataSource> dataSource;

@property (nonatomic, weak) id<YZTableViewDelegate> delegate;

// 刷新tableView

- (void)reloadData;

@end


4.实现reloadData方法,刷新表格

回顾系统如何刷新tableView

· 1.先获取有多少cell,在获取每个cell的高度。因此应该是先计算出每个cell的frame.

· 2.然后再判断当前有多少cell显示在屏幕上,就加载多少


// 刷新tableView

- (void)reloadData

{

// 这里不考虑多组,假设tableView默认只有一组。

// 先获取总共有多少cell

NSInteger rows = [self.dataSource tableView:self numberOfRowsInSection:0];

// 遍历所有cell的高度,计算每行cell的frame

CGRect cellF;

CGFloat cellX = 0;

CGFloat cellY = 0;

CGFloat cellW = self.bounds.size.width;

CGFloat cellH = 0;

CGFloat totalH = 0;

for (int i = 0; i < rows; i++) {

NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];

// 注意:这里获取的delegate,是UIScrollView中声明的属性

if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {

cellH = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];

}else{

cellH = 44;

}

cellY = i * cellH;

cellF = CGRectMake(cellX, cellY, cellW, cellH);

// 记录每个cell的y值对应的indexPath

self.indexPathDict[@(cellY)] = indexPath;

// 判断有多少cell显示在屏幕上,只加载显示在屏幕上的cell

if ([self isInScreen:cellF]) { // 当前cell的frame在屏幕上

// 通过数据源获取cell

UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];

cell.frame = cellF;

[self addSubview:cell];

}

// 添加分割线

UIView *divideV = [[UIView alloc] initWithFrame:CGRectMake(0, cellY + cellH - 1, cellW, 1)];

divideV.backgroundColor = [UIColor lightGrayColor];

divideV.alpha = 0.3;

[self addSubview:divideV];

// 添加到cell可见数组中

[self.visibleCells addObject:cell];

// 计算tableView内容总高度

totalH += cellY + cellH;

}

// 设置tableView的滚动范围

self.contentSize = CGSizeMake(self.bounds.size.width, totalH);

}


5.如何判断cell显示在屏幕上

· 当tableView内容往下走


· 当tableView内容往上走


// 根据cell尺寸判断cell在不在屏幕上

- (BOOL)isInScreen:(CGRect)cellF

{

// tableView能滚动,因此需要加上偏移量判断

// 当tableView内容往下走,offsetY会一直增加 ,cell的最大y值 < offsetY偏移量 ,cell移除屏幕

// tableView内容往上走 , offsetY会一直减少,屏幕的最大Y值 < cell的y值 ,Cell移除屏幕

// 屏幕最大y值 = 屏幕的高度 + offsetY

// 这里拿屏幕来比较,其实是因为tableView的尺寸我默认等于屏幕的高度,正常应该是tableView的高度。

// cell在屏幕上, cell的最大y值 > offsetY && cell的y值 < 屏幕的最大Y值(屏幕的高度 + offsetY)

CGFloat offsetY = self.contentOffset.y;

return CGRectGetMaxY(cellF) > offsetY && cellF.origin.y < self.bounds.size.height + offsetY;

}


6.在滚动的时候,如果有新的cell出现在屏幕上,先从缓存池中取,没有取到,在创建新的cell.


· 分析:

NO1. 需要及时监听tableView的滚动,判断下有没有新的cell出现。

NO2. 大家都会想到scrollViewDidScroll方法,这个方法只要一滚动scrollView就会调用,但是这个方法有个弊端,就是tableView内部需要作为自身的代理,才能监听,这样不好,有时候外界也需要监听滚动,因此自身类最好不要成为自己的代理。(设计思想)

· 解决:

NO1. 重写layoutSubviews,判断当前哪些cell显示在屏幕上。

NO2. 因为只要一滚动,就会修改contentOffset,就会调用layoutSubviews,其实修改contentOffset,内部其实是修改tableView的bounds,而layoutSubviews刚好是父控件尺寸一改就会调用.具体需要了解scrollView底层实现。

· 思路:

NO1. 判断下,当前tableView内容往上移动,还是往下移动,如何判断,取出显示在屏幕上的第一次cell,当前偏移量 > 第一个cell的y值,往下走。

NO2. 需要搞个数组记录下,当前有多少cell显示在屏幕上,在一开始的时候记录.


@interface YZTableView ()

@property (nonatomic, strong) NSMutableArray *visibleCells;

@end

@implementation YZTableView

@dynamic delegate;

- (NSMutableArray *)visibleCells

{

if (_visibleCells == nil) {

_visibleCells = [NSMutableArray array];

}

return _visibleCells;

}

@end


NO3. 往下移动

- 如果已经滚动到tableView内容最底部,就不需要判断新的cell,直接返回.

- 需要判断之前显示在屏幕cell有没有移除屏幕

- 只需要判断下当前可见cell数组中第一个cell有没有离开屏幕

- 只需要判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上即可。


// 判断有没有滚动到最底部

if (offsetY + self.bounds.size.height > self.contentSize.height) {

return;

}

// 判断下当前可见cell数组中第一个cell有没有离开屏幕

if ([self isInScreen:firstCell.frame] == NO) { // 如果不在屏幕

// 从可见cell数组移除

[self.visibleCells removeObject:firstCell];

// 删除第0个从可见的indexPath

[self.visibleIndexPaths removeObjectAtIndex:0];

// 添加到缓存池中

[self.reuserCells addObject:firstCell];

// 移除父控件

[firstCell removeFromSuperview];

}

// 判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上

// 这里需要计算下一个cell的y值,需要获取对应的cell的高度

// 而高度需要根据indexPath,从数据源获取

// 可以数组记录每个可见cell的indexPath的顺序,然后获取对应可见的indexPath的角标,就能获取下一个indexPath.

// 获取最后一个cell的indexPath

NSIndexPath *indexPath = [self.visibleIndexPaths lastObject];

// 获取下一个cell的indexPath

NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0];

// 获取cell的高度

if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {

cellH = [self.delegate tableView:self heightForRowAtIndexPath:nextIndexPath];

}else{

cellH = 44;

}

// 计算下一个cell的y值

cellY = lastCellY + cellH;

// 计算下下一个cell的frame

CGRect nextCellFrame = CGRectMake(cellX, cellY, cellW, cellH);

if ([self isInScreen:nextCellFrame]) { // 如果在屏幕上,就加载

// 通过数据源获取cell

UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:nextIndexPath];

cell.frame = nextCellFrame;

[self insertSubview:cell atIndex:0];

// 添加到cell可见数组中

[self.visibleCells addObject:cell];

// 添加到可见的indexPaths数组

[self.visibleIndexPaths addObject:nextIndexPath];

}


NO4. 往上移动

- 如果已经滚动到tableView最顶部,就不需要判断了有没有心的cell,直接返回.

- 需要判断之前显示在屏幕cell有没有移除屏幕

- 只需要判断下当前可见cell数组中最后一个cell有没有离开屏幕

- 只需要判断下可见cell数组中第一个cell的上一个cell显没显示在屏幕上即可

- 注意点:如果可见cell数组中第一个cell的上一个cell显示到屏幕上,一定要记得是插入到可见数组第0个的位置。

// 判断有没有滚动到最顶部

if (offsetY < 0) {

return;

}

// 判断下当前可见cell数组中最后一个cell有没有离开屏幕

if ([self isInScreen:lastCell.frame] == NO) { // 如果不在屏幕

// 从可见cell数组移除

[self.visibleCells removeObject:lastCell];

// 删除最后一个可见的indexPath

[self.visibleIndexPaths removeLastObject];

// 添加到缓存池中

[self.reuserCells addObject:lastCell];

// 移除父控件

[lastCell removeFromSuperview];

}

// 判断下可见cell数组中第一个cell的上一个cell显没显示在屏幕上

// 获取第一个cell的indexPath

NSIndexPath *indexPath = self.visibleIndexPaths[0];

// 获取下一个cell的indexPath

NSIndexPath *preIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0];

// 获取cell的高度

if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {

cellH = [self.delegate tableView:self heightForRowAtIndexPath:preIndexPath];

}else{

cellH = 44;

}

// 计算上一个cell的y值

cellY = firstCellY - cellH;

// 计算上一个cell的frame

CGRect preCellFrame = CGRectMake(cellX, cellY, cellW, cellH);

if ([self isInScreen:preCellFrame]) { // 如果在屏幕上,就加载

// 通过数据源获取cell

UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:preIndexPath];

cell.frame = preCellFrame;

[self insertSubview:cell atIndex:0];

// 添加到cell可见数组中,这里应该用插入,因为这是最上面一个cell,应该插入到数组第0个

[self.visibleCells insertObject:cell atIndex:0];

// 添加到可见的indexPaths数组,这里应该用插入,因为这是最上面一个cell,应该插入到数组第0个

[self.visibleIndexPaths insertObject:preIndexPath atIndex:0];

}

}


问题1:


· 判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上

· 这里需要计算下一个cell的frame,frame就需要计算下一个cell的y值,需要获取对应的cell的高度 cellY = lastCellY + cellH

· 而高度需要根据indexPath,从数据源获取


解决:


· 可以搞个字典记录每个可见cell的indexPath,然后获取对应可见的indexPath,就能获取下一个indexPath.

@interface YZTableView ()

// 屏幕可见数组

@property (nonatomic, strong) NSMutableArray *visibleCells;

// 缓存池

@property (nonatomic, strong) NSMutableSet *reuserCells;

// 记录每个可见cell的indexPaths的顺序

@property (nonatomic, strong) NSMutableDictionary *visibleIndexPaths;

@end

- (NSMutableDictionary *)visibleIndexPaths

{

if (_visibleIndexPaths == nil) {

_visibleIndexPaths = [NSMutableDictionary dictionary];

}

return _visibleIndexPaths;

}


注意:

· 当cell从缓存池中移除,一定要记得从可见数组cell中移除,还有可见cell的indexPath也要移除.

// 判断下当前可见cell数组中第一个cell有没有离开屏幕

if ([self isInScreen:firstCell.frame] == NO) { // 如果不在屏幕

// 从可见cell数组移除

[self.visibleCells removeObject:firstCell];

// 删除第0个从可见的indexPath

[self.visibleIndexPaths removeObjectAtIndex:0];

// 添加到缓存池中

[self.reuserCells addObject:firstCell];

}

// 判断下当前可见cell数组中最后一个cell有没有离开屏幕

if ([self isInScreen:lastCell.frame] == NO) { // 如果不在屏幕

// 从可见cell数组移除

[self.visibleCells removeObject:lastCell];

// 删除最后一个可见的indexPath

[self.visibleIndexPaths removeLastObject];

// 添加到缓存池中

[self.reuserCells addObject:lastCell];

}


7.缓存池搭建,缓存池其实就是一个NSSet集合。


· 搞一个NSSet集合充当缓存池.

· cell离开屏幕,放进缓存池

· 提供从缓存池获取方法,从缓存池中获取cell,记住要从NSSet集合移除cell.

@interface YZTableView ()

// 屏幕可见数组

@property (nonatomic, strong) NSMutableArray *visibleCells;

// 缓存池

@property (nonatomic, strong) NSMutableSet *reuserCells;

// 记录每个cell的y值都对应一个indexPath

@property (nonatomic, strong) NSMutableDictionary *indexPathDict;

@end

@implementation YZTableView

- (NSMutableSet *)reuserCells

{

if (_reuserCells == nil) {

_reuserCells = [NSMutableSet set];

}

return _reuserCells;

}

// 从缓存池中获取cell

- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier

{

UITableViewCell *cell = [self.reuserCells anyObject];

// 能取出cell,并且cell的标示符正确

if (cell && [cell.reuseIdentifier isEqualToString:identifier]) {

// 从缓存池中获取

[self.reuserCells removeObject:cell];

return cell;

}

return nil;

}

@end


8.tableView细节处理

原因:

刷新方法经常要调用

解决:

每次刷新的时候,先把之前记录的全部清空

// 刷新tableView

- (void)reloadData

{

// 刷新方法经常要调用

// 每次刷新的时候,先把之前记录的全部清空

// 清空indexPath字典

[self.indexPathDict removeAllObjects];

// 清空屏幕可见数组

[self.visibleCells removeAllObjects];

...

}

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多