前言
大家都知道UITableView,最經(jīng)典在于循環(huán)利用槽地,這里我自己模仿UITableView循環(huán)利用,寫了一套自己的TableView實現(xiàn)方案烧栋,希望大家看了我的文章,循環(huán)利用思想有顯著提升苇倡。
效果如圖:
如果喜歡我的文章富纸,可以關(guān)注我,
研究UITableView底層實現(xiàn)
1.系統(tǒng)UITabelView的簡單使用旨椒,這里就不考慮分組了晓褪,默認(rèn)為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的實現(xiàn)機制综慎。
如圖打印結(jié)果:
分析:底層先獲取有多少cell(10個)涣仿,在獲取每個cell的高度,返回高度的方法一開始調(diào)用10次示惊。
目的:確定tableView的滾動范圍
变过,一開始計算所有cell的frame,就能計算下tableView的滾動范圍。
分析:tableView:cellForRowAtIndexPath:方法什么時候調(diào)用涝涤。
打印驗證媚狰,如圖:
一開始調(diào)用了7次,因為一開始屏幕最多顯示7個cell
目的:一開始只加載顯示出來的cell阔拳,等有新的cell出現(xiàn)的時候會繼續(xù)調(diào)用這個方法加載cell崭孤。
3.UITableView循環(huán)利用思想
當(dāng)新的cell出現(xiàn)的時候,首先從緩存池中獲取糊肠,如果沒有獲取到辨宠,就自己創(chuàng)建cell。
當(dāng)有cell移除屏幕的時候货裹,把cell放到緩存池中去嗤形。
二、自定義UIScrollView弧圆,模仿UITableView循環(huán)利用
1.提供數(shù)據(jù)源和代理方法赋兵,命名和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.提供代理和數(shù)據(jù)源屬性
@interface YZTableView : UIScrollView
@property (nonatomic, weak) id<YZTableViewDataSource> dataSource;
@property (nonatomic, weak) id<YZTableViewDelegate> delegate;
@end
警告:
解決搔预,在YZTableView.m的實現(xiàn)中聲明霹期。
原因:有人會問為什么我要定義同名的delegate屬性,我主要想模仿系統(tǒng)的tableView拯田,系統(tǒng)tableView也有同名的屬性历造。
思路:這樣做,外界在使用設(shè)置我的tableView的delegate,就必須遵守的我的代理協(xié)議吭产,而不是UIScrollView的代理協(xié)議侣监。
3.提供刷新方法reloadData
,因為tableView通過這個刷新tableView臣淤。
@interface YZTableView : UIScrollView
@property (nonatomic, weak) id<YZTableViewDataSource> dataSource;
@property (nonatomic, weak) id<YZTableViewDelegate> delegate;
// 刷新tableView
- (void)reloadData;
@end
4.實現(xiàn)reloadData方法橄霉,刷新表格
-
回顧系統(tǒng)如何刷新tableView
- 1.先獲取有多少cell,在獲取每個cell的高度。因此應(yīng)該是先計算出每個cell的frame.
- 2.然后再判斷當(dāng)前有多少cell顯示在屏幕上荒典,就加載多少
// 刷新tableView
- (void)reloadData
{
// 這里不考慮多組酪劫,假設(shè)tableView默認(rèn)只有一組。
// 先獲取總共有多少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值對應(yīng)的indexPath
self.indexPathDict[@(cellY)] = indexPath;
// 判斷有多少cell顯示在屏幕上,只加載顯示在屏幕上的cell
if ([self isInScreen:cellF]) { // 當(dāng)前cell的frame在屏幕上
// 通過數(shù)據(jù)源獲取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可見數(shù)組中
[self.visibleCells addObject:cell];
// 計算tableView內(nèi)容總高度
totalH += cellY + cellH;
}
// 設(shè)置tableView的滾動范圍
self.contentSize = CGSizeMake(self.bounds.size.width, totalH);
}
5.如何判斷cell顯示在屏幕上
-
當(dāng)tableView內(nèi)容往下走
當(dāng)tableView內(nèi)容往下走.gif 當(dāng)tableView內(nèi)容往上走
// 根據(jù)cell尺寸判斷cell在不在屏幕上
- (BOOL)isInScreen:(CGRect)cellF
{
// tableView能滾動,因此需要加上偏移量判斷
// 當(dāng)tableView內(nèi)容往下走遮咖,offsetY會一直增加 ,cell的最大y值 < offsetY偏移量 ,cell移除屏幕
// tableView內(nèi)容往上走 , offsetY會一直減少滩字,屏幕的最大Y值 < cell的y值 ,Cell移除屏幕
// 屏幕最大y值 = 屏幕的高度 + offsetY
// 這里拿屏幕來比較御吞,其實是因為tableView的尺寸我默認(rèn)等于屏幕的高度麦箍,正常應(yīng)該是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出現(xiàn)在屏幕上
,先從緩存池中取揍诽,沒有取到诀蓉,在創(chuàng)建新的cell.
分析:
- 需要及時監(jiān)聽tableView的滾動,判斷下有沒有新的cell出現(xiàn)暑脆。
- 大家都會想到scrollViewDidScroll方法渠啤,這個方法只要一滾動scrollView就會調(diào)用,但是這個方法有個弊端添吗,就是tableView內(nèi)部需要作為自身的代理沥曹,才能監(jiān)聽,這樣不好碟联,有時候外界也需要監(jiān)聽滾動妓美,因此
自身類最好不要成為自己的代理
。(設(shè)計思想
)
解決:
- 重寫layoutSubviews玄帕,判斷當(dāng)前哪些cell顯示在屏幕上部脚。
- 因為只要一滾動,就會修改contentOffset,就會調(diào)用layoutSubviews裤纹,其實修改contentOffset,內(nèi)部其實是修改tableView的bounds,而
layoutSubviews
剛好是父控件尺寸一改
就會調(diào)用
.具體需要了解scrollView底層實現(xiàn)。
思路:
判斷下鹰椒,當(dāng)前tableView內(nèi)容往上移動锡移,還是往下移動,如何判斷漆际,取出顯示在屏幕上的第一次cell淆珊,
當(dāng)前偏移量 > 第一個cell的y值
,往下走奸汇。需要搞個數(shù)組記錄下施符,當(dāng)前有多少cell顯示在屏幕上,在一開始的時候記錄.
@interface YZTableView ()
@property (nonatomic, strong) NSMutableArray *visibleCells;
@end
@implementation YZTableView
@dynamic delegate;
- (NSMutableArray *)visibleCells
{
if (_visibleCells == nil) {
_visibleCells = [NSMutableArray array];
}
return _visibleCells;
}
@end
- 往下移動
- 如果已經(jīng)滾動到tableView內(nèi)容最底部擂找,就不需要判斷新的cell戳吝,直接返回.
- 需要判斷之前顯示在屏幕cell有沒有移除屏幕
- 只需要判斷下當(dāng)前可見cell數(shù)組中第一個cell有沒有離開屏幕
- 只需要判斷下當(dāng)前可見cell數(shù)組中最后一個cell的下一個cell顯沒顯示在屏幕上即可。
// 判斷有沒有滾動到最底部
if (offsetY + self.bounds.size.height > self.contentSize.height) {
return;
}
// 判斷下當(dāng)前可見cell數(shù)組中第一個cell有沒有離開屏幕
if ([self isInScreen:firstCell.frame] == NO) { // 如果不在屏幕
// 從可見cell數(shù)組移除
[self.visibleCells removeObject:firstCell];
// 刪除第0個從可見的indexPath
[self.visibleIndexPaths removeObjectAtIndex:0];
// 添加到緩存池中
[self.reuserCells addObject:firstCell];
// 移除父控件
[firstCell removeFromSuperview];
}
// 判斷下當(dāng)前可見cell數(shù)組中最后一個cell的下一個cell顯沒顯示在屏幕上
// 這里需要計算下一個cell的y值贯涎,需要獲取對應(yīng)的cell的高度
// 而高度需要根據(jù)indexPath听哭,從數(shù)據(jù)源獲取
// 可以數(shù)組記錄每個可見cell的indexPath的順序,然后獲取對應(yīng)可見的indexPath的角標(biāo),就能獲取下一個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]) { // 如果在屏幕上塘雳,就加載
// 通過數(shù)據(jù)源獲取cell
UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:nextIndexPath];
cell.frame = nextCellFrame;
[self insertSubview:cell atIndex:0];
// 添加到cell可見數(shù)組中
[self.visibleCells addObject:cell];
// 添加到可見的indexPaths數(shù)組
[self.visibleIndexPaths addObject:nextIndexPath];
}
- 往上移動
- 如果已經(jīng)滾動到tableView最頂部陆盘,就不需要判斷了有沒有心的cell,直接返回.
- 需要判斷之前顯示在屏幕cell有沒有移除屏幕
- 只需要判斷下當(dāng)前可見cell數(shù)組中最后一個cell有沒有離開屏幕
- 只需要判斷下可見cell數(shù)組中第一個cell的上一個cell顯沒顯示在屏幕上即可
- 注意點:如果可見cell數(shù)組中第一個cell的上一個cell顯示到屏幕上败明,一定要記得是
插入到可見數(shù)組第0個的位置
隘马。
// 判斷有沒有滾動到最頂部
if (offsetY < 0) {
return;
}
// 判斷下當(dāng)前可見cell數(shù)組中最后一個cell有沒有離開屏幕
if ([self isInScreen:lastCell.frame] == NO) { // 如果不在屏幕
// 從可見cell數(shù)組移除
[self.visibleCells removeObject:lastCell];
// 刪除最后一個可見的indexPath
[self.visibleIndexPaths removeLastObject];
// 添加到緩存池中
[self.reuserCells addObject:lastCell];
// 移除父控件
[lastCell removeFromSuperview];
}
// 判斷下可見cell數(shù)組中第一個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]) { // 如果在屏幕上,就加載
// 通過數(shù)據(jù)源獲取cell
UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:preIndexPath];
cell.frame = preCellFrame;
[self insertSubview:cell atIndex:0];
// 添加到cell可見數(shù)組中,這里應(yīng)該用插入妻顶,因為這是最上面一個cell,應(yīng)該插入到數(shù)組第0個
[self.visibleCells insertObject:cell atIndex:0];
// 添加到可見的indexPaths數(shù)組,這里應(yīng)該用插入酸员,因為這是最上面一個cell,應(yīng)該插入到數(shù)組第0個
[self.visibleIndexPaths insertObject:preIndexPath atIndex:0];
}
}
問題1:
判斷下當(dāng)前可見cell數(shù)組中最后一個cell的下一個cell顯沒顯示在屏幕上
這里需要計算下一個cell的frame,frame就需要計算下一個cell的y值,需要獲取對應(yīng)的cell的高度
cellY = lastCellY + cellH
而高度需要根據(jù)indexPath盈包,從數(shù)據(jù)源獲取
解決:
- 可以搞個字典記錄每個可見cell的indexPath,然后獲取對應(yīng)可見的indexPath沸呐,就能獲取下一個indexPath.
@interface YZTableView ()
// 屏幕可見數(shù)組
@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;
}
注意:
- 當(dāng)cell從緩存池中移除,一定要記得從可見數(shù)組cell中移除呢燥,還有可見cell的indexPath也要移除.
// 判斷下當(dāng)前可見cell數(shù)組中第一個cell有沒有離開屏幕
if ([self isInScreen:firstCell.frame] == NO) { // 如果不在屏幕
// 從可見cell數(shù)組移除
[self.visibleCells removeObject:firstCell];
// 刪除第0個從可見的indexPath
[self.visibleIndexPaths removeObjectAtIndex:0];
// 添加到緩存池中
[self.reuserCells addObject:firstCell];
}
// 判斷下當(dāng)前可見cell數(shù)組中最后一個cell有沒有離開屏幕
if ([self isInScreen:lastCell.frame] == NO) { // 如果不在屏幕
// 從可見cell數(shù)組移除
[self.visibleCells removeObject:lastCell];
// 刪除最后一個可見的indexPath
[self.visibleIndexPaths removeLastObject];
// 添加到緩存池中
[self.reuserCells addObject:lastCell];
}
7.緩存池搭建崭添,緩存池其實就是一個NSSet集合
。
- 搞一個NSSet集合充當(dāng)緩存池.
- cell離開屏幕叛氨,放進緩存池
- 提供從緩存池獲取方法呼渣,從緩存池中獲取cell,記住要從NSSet集合移除cell.
@interface YZTableView ()
// 屏幕可見數(shù)組
@property (nonatomic, strong) NSMutableArray *visibleCells;
// 緩存池
@property (nonatomic, strong) NSMutableSet *reuserCells;
// 記錄每個cell的y值都對應(yīng)一個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的標(biāo)示符正確
if (cell && [cell.reuseIdentifier isEqualToString:identifier]) {
// 從緩存池中獲取
[self.reuserCells removeObject:cell];
return cell;
}
return nil;
}
@end
8.tableView細(xì)節(jié)處理
原因:
刷新方法經(jīng)常要調(diào)用
解決:
每次刷新的時候,先把之前記錄的全部清空
// 刷新tableView
- (void)reloadData
{
// 刷新方法經(jīng)常要調(diào)用
// 每次刷新的時候寞埠,先把之前記錄的全部清空
// 清空indexPath字典
[self.indexPathDict removeAllObjects];
// 清空屏幕可見數(shù)組
[self.visibleCells removeAllObjects];
...
}