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

研究UITableView底层实现

系统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
@required
// 返回有多少行cell

  • (NSInteger)tableView:(YZTableView *)tableView numberOfRowsInSection:(NSInteger)section;
    // 返回每行cell长什么样子
  • (UITableViewCell )tableView:(YZTableView )tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
    @end
    @protocol YZTableViewDelegate
    // 返回每行cell有多高
  • (CGFloat)tableView:(YZTableView )tableView heightForRowAtIndexPath:(NSIndexPath )indexPath;
    @end

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

@interface YZTableView : UIScrollView
@property (nonatomic, weak) id dataSource;
@property (nonatomic, weak) id delegate;
@end

警告:

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

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

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

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

@interface YZTableView : UIScrollView
@property (nonatomic, weak) id dataSource;
@property (nonatomic, weak) id 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];
    ...
    }

iOS探究UITableView的内部代码,仿UITableView自定义的更多相关文章

  1. ios学习笔记 UITableView(纯代码) (二)

    头文件 --------------------------------------------- #import <UIKit/UIKit.h> /** UITableViewDataS ...

  2. ios基础篇(十四)——UITableView(二)属性及基本用法

    上一篇说了UITableView的重用机制,让我们对UITableView有了简单了解,下面说说UITableView的属性及常见方法. 一.属性 1.frame:设置控件的尺寸和大小 2.backg ...

  3. iOS深入学习(UITableView系列4:使用xib自定义cell)

    可以通过继承UITableViewCell重新自定义cell,可以像下面一样通过代码来自定义cell,但是手写代码总是很浪费时间, ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ...

  4. iOS - Xcode项目统计总代码行数

    最新公司需要把项目代码量统计一下,第一时间找到Xcode插件管理工具Alcatraz,查找插件ZLXCodeLine,这是一个快速统计Xcode工程项目代码量的插件,好像已经不支持Alcatraz安装 ...

  5. iOS开发UI篇—从代码的逐步优化看MVC

    iOS开发UI篇—从代码的逐步优化看MVC 一.要求 要求完成下面一个小的应用程序. 二.一步步对代码进行优化 注意:在开发过程中,优化的过程是一步一步进行的.(如果一个人要吃五个包子才能吃饱,那么他 ...

  6. 【iOS 使用github上传代码】详解

    [iOS 使用github上传代码]详解 一.github创建新工程 二.直接添加文件 三.通过https 和 SSH 操作两种方式上传工程 3.1https 和 SSH 的区别: 3.1.1.前者可 ...

  7. Assertion failure in -[UITableView _classicHeightForRowAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-3318/UITableView.m:10772

    Assertion failure in -[UITableView _classicHeightForRowAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-3 ...

  8. IOS 推送-配置与代码编写

    IOS 推送配置与代码编写 这里介绍IOS的推送,本文章已经在IOS6/7/8上都能运行OK,按照道理IOS9应该没问题. 大纲: 1.文章前提 2.推送介绍 3.推送文件账号设置 4.推送证书介绍 ...

  9. iOS开发数据库篇—SQL代码应用示例

    iOS开发数据库篇—SQL代码应用示例 一.使用代码的方式批量添加(导入)数据到数据库中 1.执行SQL语句在数据库中添加一条信息 插入一条数据的sql语句: 点击run执行语句之后,刷新数据 2.在 ...

随机推荐

  1. js中对style中的多个属性进行设值

    js中对style中的多个属性进行设值: 看一下案例自然就明白: document.getElementById("my_wz1").style.cssText="bac ...

  2. linux daemon

    参考 鸟哥的私房菜 http://linux.vbird.org/linux_basic/0560daemons.php

  3. Webkit之资源加载

    一.webkit资源分类 webkit中有多种资源,大致分为以下几种: HTML文本 CSS样式文本 - CachedCSSStyleSheet 字体 - CachedFont 图片 - Cached ...

  4. ABI & API

    API defines the programning language and function entry point, arguments type, order. ABI defines th ...

  5. 添加一个Application Framework Service

    如何添加一个Application Framework Service(without native code)? 1.本文参照AlarmManagerService实现一个简单的Applicatio ...

  6. 二分图最大匹配 Hopcroft-Karp算法模板

    #include <iostream> #include <cstdlib> #include <cstdio> #include <cstring> ...

  7. LISTVIEW嵌套GRIDVIEW的一些处理(点击GRIDVIEW的条目,能够显示他在LISTVIEW中的位置)(对这篇文章的优化处理,不每次都new onItemClickListener)

    前几天写了点击GRIDVIEW的条目,能够显示他在LISTVIEW中的位置,当时的处理是在ListView的适配器里的GetView方法里每次都new GridView的onItemClickList ...

  8. Android中在布局中写ViewPager无法渲染出来的问题

    今天刚刚使用Android Studio,在写ViewPager时,首先按F4把Dependencies添加一个V4包,然后写ViewPager,如下: <android.support.v4. ...

  9. P2P应用中的NAT穿透问题

    多年前曾经写过一个关于NAT钻洞的实验.现在发现那个做法在我现在的路由器上已经不管用了.经过一番搜索发现时过境迁,世界变化很快,新路由器已经是UPnP了.在这里重新理一下几种方法. 第一种,也是不太靠 ...

  10. notify vs nofifyall

    http://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again Do you want to tell ...