你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 阿峥教你實現UITableView循環利用

阿峥教你實現UITableView循環利用

編輯:IOS開發基礎

前言

大家都知道UITableView,最經典在於循環利用,這裡我自己模仿UITableView循環利用,寫了一套自己的TableView實現方案,希望大家看了我的文章,循環利用思想有顯著提升。

效果如圖:

304825-6ff3a08ced53b208.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的實現機制。

如圖打印結果:

03.png

分析:底層先獲取有多少cell(10個),在獲取每個cell的高度,返回高度的方法一開始調用10次。

目的:確定tableView的滾動范圍,一開始計算所有cell的frame,就能計算下tableView的滾動范圍。

分析:tableView:cellForRowAtIndexPath:方法什麼時候調用。

打印驗證,如圖:

04.png

一開始調用了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

警告:

05.png

解決,在YZTableView.m的實現中聲明。

06.png

原因:有人會問為什麼我要定義同名的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內容往下走

01.gif

  • 當tableView內容往上走

02.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
  • 往下移動

1.如果已經滾動到tableView內容最底部,就不需要判斷新的cell,直接返回.

2.需要判斷之前顯示在屏幕cell有沒有移除屏幕

3.只需要判斷下當前可見cell數組中第一個cell有沒有離開屏幕

4.只需要判斷下當前可見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];
            
            
        }
  • 往上移動

1.如果已經滾動到tableView最頂部,就不需要判斷了有沒有心的cell,直接返回.

2.需要判斷之前顯示在屏幕cell有沒有移除屏幕

3.只需要判斷下當前可見cell數組中最後一個cell有沒有離開屏幕

4.只需要判斷下可見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];
    ...
}

如果你喜歡這篇文章,可以繼續關注我,微博:吖了個峥,歡迎交流。

點擊這下載源代碼。
(PS:另外咱們公司小碼哥,誠邀IT屆有事業心,有能力,有拼勁,有干勁各路英豪加盟一起創業,詳情可以點擊小碼哥,小碼哥官方微博,或者微博私聊我)

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved