iOS探究UITableView的内部代码,仿UITableView自定义
大家都知道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循环利用
- 提供数据源和代理方法,命名和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自定义的更多相关文章
- ios学习笔记 UITableView(纯代码) (二)
头文件 --------------------------------------------- #import <UIKit/UIKit.h> /** UITableViewDataS ...
- ios基础篇(十四)——UITableView(二)属性及基本用法
上一篇说了UITableView的重用机制,让我们对UITableView有了简单了解,下面说说UITableView的属性及常见方法. 一.属性 1.frame:设置控件的尺寸和大小 2.backg ...
- iOS深入学习(UITableView系列4:使用xib自定义cell)
可以通过继承UITableViewCell重新自定义cell,可以像下面一样通过代码来自定义cell,但是手写代码总是很浪费时间, ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ...
- iOS - Xcode项目统计总代码行数
最新公司需要把项目代码量统计一下,第一时间找到Xcode插件管理工具Alcatraz,查找插件ZLXCodeLine,这是一个快速统计Xcode工程项目代码量的插件,好像已经不支持Alcatraz安装 ...
- iOS开发UI篇—从代码的逐步优化看MVC
iOS开发UI篇—从代码的逐步优化看MVC 一.要求 要求完成下面一个小的应用程序. 二.一步步对代码进行优化 注意:在开发过程中,优化的过程是一步一步进行的.(如果一个人要吃五个包子才能吃饱,那么他 ...
- 【iOS 使用github上传代码】详解
[iOS 使用github上传代码]详解 一.github创建新工程 二.直接添加文件 三.通过https 和 SSH 操作两种方式上传工程 3.1https 和 SSH 的区别: 3.1.1.前者可 ...
- Assertion failure in -[UITableView _classicHeightForRowAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-3318/UITableView.m:10772
Assertion failure in -[UITableView _classicHeightForRowAtIndexPath:], /SourceCache/UIKit_Sim/UIKit-3 ...
- IOS 推送-配置与代码编写
IOS 推送配置与代码编写 这里介绍IOS的推送,本文章已经在IOS6/7/8上都能运行OK,按照道理IOS9应该没问题. 大纲: 1.文章前提 2.推送介绍 3.推送文件账号设置 4.推送证书介绍 ...
- iOS开发数据库篇—SQL代码应用示例
iOS开发数据库篇—SQL代码应用示例 一.使用代码的方式批量添加(导入)数据到数据库中 1.执行SQL语句在数据库中添加一条信息 插入一条数据的sql语句: 点击run执行语句之后,刷新数据 2.在 ...
随机推荐
- spring @Component
使用 @Component <context:component-scan base-package="dao" /> 虽 然我们可以通过@Autowired或@R ...
- Spring Boot 系列教程9-swagger-前后端分离后的标准
前后端分离的必要 现在的趋势发展,需要把前后端开发和部署做到真正的分离 做前端的谁也不想用Maven或者Gradle作为构建工具 做后端的谁也不想要用Grunt或者Gulp作为构建工具 前后端需要通过 ...
- JAVA-基本知识
1.JAVA跨平台 其实就是在每个平台上要安装对应该操作系统的JVM,JVM负责解析执行,即实现了跨平台.JVM是操作系统与java程序之间的桥梁. 2.JRE:java运行环境,包含JVM+核心类库 ...
- c++运行时类型识别(rtti)
一个简单运行时类型识别 namespace rtti_ex { /* * 类型信息基类 */ class i_type_info { public: // 判断是否是指定类型 bool is(cons ...
- 字段为空sql语句,设置当前模式
delete from t_corpinfo where CORPID='' and CORPNAME='' 该命令是删除字段为空的记录 SET CURRENT SCHEMA DB2INST1;
- HDU 4460 Friend Chains(map + spfa)
Friend Chains Time Limit : 2000/1000ms (Java/Other) Memory Limit : 32768/32768K (Java/Other) Total ...
- 【Qt开发】修改源码文件的编码格式的小技巧 .
默认情况下,代码文件应该以utf-8的格式来存储的.而如果在代码文件的转移或者上传下载过程中,弄乱了文件的编码格式,一般会出现乱码的情况. 例如windows系统下,中文就很容易出现乱码,如下图,文件 ...
- Oracle Sql优化之分层查询(connect by)
1.对于表中行与行存在父子关系时,可以通过connect by查询方式,查询行与行之间的父子关系 ),'-')|| empno as tempno, ename,mgr,level, decode(, ...
- 转:LoadRunner检查点使用小结
LR中检查点有两种:图片和文字. 常用检查点函数如下: 1)web_find()函数用于从 HTML 页中搜索指定的文本字符串: 2)web_reg_find()函数注册一个请求,以在下一个操作函数( ...
- mysql笔记6之数据类型
1 区别一: varchar:可变长度的字符串.根据添加的数据长度决定占用的字符数 char:固定长度的字符串 2区别二 int:没有限制 int(4):限制为4 3 区别三: 日期: date ...