【阿峥教你实现UITableView循环利用】 |那些人追的干货

前言

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

tableView效果.gif

如果喜欢我的文章,可以关注我,

研究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的实现机制。

如图打印结果:

Snip20150808_3.png

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

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

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

Snip20150808_5.png

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

3.UITableView循环利用思想

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

二、自定义UIScrollView,模仿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

警告:

Snip20150816_1.png

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

Snip20150816_2.png

原因:有人会问为什么我要定义同名的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内容往下走.gif
  • 当tableView内容往上走

当tableView内容往上走.gif

// 根据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.

分析:

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

解决:

  • 重写layoutSubviews,判断当前哪些cell显示在屏幕上。
  • 因为只要一滚动,就会修改contentOffset,就会调用layoutSubviews,其实修改contentOffset,内部其实是修改tableView的bounds,而layoutSubviews刚好是父控件尺寸一改就会调用.具体需要了解scrollView底层实现

思路:

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

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

@interface YZTableView ()

@property (nonatomic, strong) NSMutableArray *visibleCells;

@end


@implementation YZTableView

@dynamic delegate;

- (NSMutableArray *)visibleCells
{
    if (_visibleCells == nil) {
        _visibleCells = [NSMutableArray array];
    }
    return _visibleCells;
}
@end

  • 往下移动
    • 如果已经滚动到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];



        }

  • 往上移动
    • 如果已经滚动到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];
    ...
}

联系方式

如果你喜欢这篇文章,可以继续关注我,微博:吖了个峥,欢迎交流。
点击这下载源代码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,154评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,252评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,107评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,985评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,905评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,256评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,978评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,611评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,891评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,910评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,736评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,516评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,995评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,034评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,242评论 2 339

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,966评论 4 60
  • 概述在iOS开发中UITableView可以说是使用最广泛的控件,我们平时使用的软件中到处都可以看到它的影子,类似...
    liudhkk阅读 8,925评论 3 38
  • 在简书里 我是谁 天南地北 天涯海角的 我和 你 相聚在简书里 我不会告诉你 我是位全职妈妈 每天与孩子相伴 我不...
    明希美美阅读 267评论 6 5
  • 露重深秋夜 披衣月下行 空吟无友对 抱憾意难平
    江南丹橘_a43d阅读 386评论 5 14