溫故知新 - UITableView

內容目錄

  1. UITableView初始化/頭視圖/編輯模式
  2. 代理方法及常用方法邏輯
  3. cell的重用原理(重難點)
  4. UITableView&UITableViewCell的常見設置
  5. 數據刷新方法及原則
  6. UITabelView性能優化

Declaration

@interface UITableView : UIScrollView <NSCoding, UIDataSourceTranslating>

可以看到UITableView是繼承於UIScrollView,而UIScrollView繼承與UIView,所以父類的方法都是可以使用的,最常用的是使用ScrollView的移動就觸發的代理方法-(void)scrollViewDidScroll:(UIScrollView *)scrollView;

初始化

通過懶加載展示初始化的基本流程,

- (UITableView *)tableView{
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];    //初始化
        _tableView.dataSource = self;   //代理方法
        _tableView.delegate = self;     //代理方法
        _tableView.rowHeight = 44;      //設置行高
        _tableView.showsVerticalScrollIndicator = NO;    //不出現滾動條
        _tableView.backgroundColor = [UIColor redColor];   //設置背景顏色
        // 分割線顏色
        _tableView.separatorColor = [UIColor redColor];
        // 隱藏分割線
        _tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    }
    return _tableView;
}

頭視圖設置

第一種
self.tableView.tableHeaderView=self.imageView;
第二種
self.tableView.contentInset=UIEdgeInsetsMake(200, 0, 0, 0);

代理方法

上面初始化賦值的兩個代理對應<UITableViewDelegate,UITableViewDataSource>UITableViewDataSource中有兩個必須實現的方法numberOfRowsInSection,cellForRowAtIndexPath,協議常用方法如下

/**
 *  告訴tableView一共有多少組數據(設置分組數量)
 */
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

/**
 *  告訴tableView第section組有多少行(設置每組cell的數量)
 */
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section

/**
 *  告訴tableView第indexPath行顯示怎樣的cell(cell內容)
 */
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

/**
 *  告訴tableView第section組的頭部標題
 */
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section

/**
 *  告訴tableView第section組的尾部標題
 */
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section

/**
 *  點擊tableView的cell觸發的點擊事件方法
 */
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath

/**
 *  設置tableView每行的高度(一些需要動態計算高度的cell可以用這個方法去做最後的高度處理)
 */
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;

/**
 *  設置行高(標題Header的行高)
 */
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;

/**
 *  設置Header內容
 */
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;

/**
 *  設置行高(標題Footer的行高)
 */
-(CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

/**
 *  設置Footer內容
 */
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section;

/**
 *  記錄上一次的點擊效果(延遲點擊)
 */
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath;

代理方法常用邏輯

根據Index獲取當前的cell,這裏以collectionView方法爲例

#pragma  mark collectionView的點擊方法
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    
    //CollectionView的聯動方法,讓bodyCollectionView隨點擊聯動,有動畫
    [self.bodyCollectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionNone animated:YES];
    //判斷是否爲titleCollectionView
    if(collectionView==self.titleCollectionView){
        //獲取當前的cell
        GY_TitleCollectionViewCell *cell = (GY_TitleCollectionViewCell *)[self.titleCollectionView cellForItemAtIndexPath:indexPath];
        //遍歷cell裏的四圖片
        for (int i = 0;  i < 4; i++) {
            if (indexPath.row == i) {
                //高亮,高亮顏色
                cell.label.highlighted=YES;
                cell.label.highlightedTextColor=[UIColor orangeColor];
                cell.label.backgroundColor=[UIColor colorWithRed:13/255.0 green:28/255.0 blue:40/255.0 alpha:1];
            }
            else
            {
                //如果不是點擊的那張位置的圖片,設置爲灰色,注意:indexpath是自定義的(系統的indexPath爲CollectionView的個數),自定義的是當前CollectionView,對應的cell
                NSIndexPath *indexpath = [NSIndexPath indexPathForItem: i  inSection:0];
                GY_TitleCollectionViewCell *cell = (GY_TitleCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexpath];
                cell.label.highlighted=NO;
                cell.label.highlightedTextColor=[UIColor orangeColor];
                cell.label.backgroundColor=[UIColor colorWithRed:13/255.0 green:29/255.0 blue:53/255.0 alpha:1];
            }
        }
    }
}

根據當前偏移量設置UI(該邏輯爲卡在64導航欄高度的位置)

#pragma mark --------移動就觸發的方法
-(void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (scrollView == self.tableView) {

    CGFloat y0ffest=self.tableView.contentOffset.y;
    NSLog(@"%f",y0ffest);
    if (self.tableView.contentOffset.y<=-250) {
        self.heroImage.frame =CGRectMake(250+y0ffest, y0ffest, self.tableView.frame.size.width-(250+y0ffest)*2, -y0ffest);
        self.effectView.frame=CGRectMake(250+y0ffest, y0ffest, self.tableView.frame.size.width-(250+y0ffest)*2, -y0ffest);
        self.displayNameLabel.frame=CGRectMake(20-250-y0ffest, 70-250-y0ffest, 120, 40);
        self.titleLabel.frame=CGRectMake(20-250-y0ffest, 105-250-y0ffest, 40, 35);
    }
    else if(self.tableView.contentOffset.y >=-64){
        self.heroImage.frame = CGRectMake(0, -250+y0ffest+64, self.tableView.frame.size.width, 250);
        self.tableView.contentInset=UIEdgeInsetsMake(64, 0, 0, 0);
    }
    else{
        self.displayNameLabel.alpha=1-0.005*(y0ffest+290);
        self.titleLabel.alpha=1-0.005*(y0ffest+290);
    }
    }
}

根據cell自動計算高度

//設置cell高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    RedPersonCell *cell = (RedPersonCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
    return cell.height;
}

取消cell選中狀態

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    // 不加此句時,在二級欄目點擊返回時,此行會由選中狀態慢慢變成非選中狀態。
    // 加上此句,返回時直接就是非選中狀態。
}

UITableViewCell

這個控件是系統爲我們提供的api,是一個提供給你調用快捷開發的api,結構及默認形態如下圖



當然,如果這個控件滿足不了UI的需求,就需要自定義cell了!

重用池機制

問題來了:iOS設備的內存有限,如果用UITableView顯示成千上萬條數據,就需要成千上萬個UITableViewCell對象的話,那將會耗盡iOS設備的內存。要解決該問題,需要重用UITableViewCell對象

重用原理:當滾動列表時,部分UITableViewCell會移出窗口,UITableView會將窗口外的UITableViewCell放入一個對象池中,等待重用。當UITableView要求dataSource返回UITableViewCell時,dataSource會先查看這個對象池,如果池中有未使用的UITableViewCell,dataSource會用新的數據配置這個UITableViewCell,然後返回給UITableView,重新顯示到窗口中,從而避免創建新對象

還有一個非常重要的問題:有時候需要自定義UITableViewCell(用一個子類繼承UITableViewCell),而且每一行用的不一定是同一種UITableViewCell,所以一個UITableView可能擁有不同類型的UITableViewCell,對象池中也會有很多不同類型的UITableViewCell,那麼UITableView在重用UITableViewCell時可能會得到錯誤類型的UITableViewCell

解決方案:UITableViewCell有個NSString *reuseIdentifier屬性,可以在初始化UITableViewCell的時候傳入一個特定的字符串標識來設置reuseIdentifier(一般用UITableViewCell的類名)。當UITableView要求dataSource返回UITableViewCell時,先通過一個字符串標識到對象池中查找對應類型的UITableViewCell對象,如果有,就重用,如果沒有,就傳入這個字符串標識來初始化一個UITableViewCell對象

Cell重用實現代碼

/**
 *  什麼時候調用:每當有一個cell進入視野範圍內就會調用
 */
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // 0.重用標識
    // 被static修飾的局部變量:只會初始化一次,在整個程序運行過程中,只有一份內存
    static NSString *ID = @"cell";

    // 1.先根據cell的標識去緩存池中查找可循環利用的cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];

    // 2.如果cell爲nil(緩存池找不到對應的cell)
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
        // 因爲對於一個cell這裏只會走一次,是初始化cell的方法,因爲cellForRowAtIndexPath在滑動時會被頻繁調用,所以可以在這裏面寫只想運行一次的代碼
    }

    // 3.覆蓋數據(需要動態設置的值在這裏處理)
    cell.textLabel.text = [NSString stringWithFormat:@"testdata - %zd", indexPath.row];

    return cell;
}

另一種註冊cell方法

// 在這個方法中註冊cell
- (void)viewDidLoad {
    [super viewDidLoad];

    // 註冊某個標識對應的cell類型
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:ID];
}

UITableViewCell的常見設置

// 取消選中的樣式
cell.selectionStyle = UITableViewCellSelectionStyleNone;
// backgroundView的優先級 > backgroundColor
// 設置選中的背景色
UIView *selectedBackgroundView = [[UIView alloc] init];
selectedBackgroundView.backgroundColor = [UIColor redColor];
cell.selectedBackgroundView = selectedBackgroundView;

// 設置默認的背景色
cell.backgroundColor = [UIColor blueColor];

// 設置默認的背景色
UIView *backgroundView = [[UIView alloc] init];
backgroundView.backgroundColor = [UIColor greenColor];
cell.backgroundView = backgroundView;

// 設置指示器(顯示在視圖右邊, 有>箭頭,✔️等等效果)
//    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.accessoryView = [[UISwitch alloc] init];

自定義cell

創建一個繼承自UITableViewCell的子類,比如MyCell

#import <UIKit/UIKit.h>
//父類是UIView  不是UITableView
@interface MyCell : UITableViewCell
//cell本身就提供了三個屬性視圖,所以爲了避免衝突,一定不要自定義的cell的屬性名和系統的衝突

//姓名和聯繫方式的label
@property(nonatomic,retain)UILabel *namelabel;
@property(nonatomic,retain)UILabel *phonelabel;
@end
//要實現自定義的cell,一般重寫兩個方法
//一.初始化方法
-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier{
    [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        [self createView];
    }
    return self;
}

-(void)createView{
    //在裏面只創建視圖,不設置尺寸
    self.namelabel=[[UILabel alloc] init];
    self.namelabel.backgroundColor=[UIColor yellowColor];
    //把屬性視圖放到contentView上顯示
    [self.contentView addSubview:self.namelabel];
    
    self.phonelabel=[[UILabel alloc] init];
    self.phonelabel.backgroundColor=[UIColor cyanColor];
    [self.contentView addSubview:self.phonelabel];
}

//重寫第二個方法
//這個方法是整個cell在出現前所執行的最後一個方法,所以爲了能精準的設置它的尺寸,在這個方法裏寫空間尺寸的設置
-(void)layoutSubviews{
    //如果不寫,佈局可能會出現問題,所以別忘記了!!!!!!
    [super layoutSubviews];
    //在這個方法裏只設置尺寸
    self.namelabel.frame = CGRectMake(0, 0, WIDTH/2, HEIGHT/2);
    self.phonelabel.frame = CGRectMake(WIDTH/2, HEIGHT/2, WIDTH/2, HEIGHT/2);
}

編輯模式

//開啓一下tableView的編輯模式
[self.tableView setEditing:YES animated:YES];
#pragma mark 設置點擊進入編輯模式!
//重寫一下系統提供的編輯按鈕的點擊方法
-(void)setEditing:(BOOL)editing animated:(BOOL)animated{
    [super setEditing:editing animated:animated];
    [self.tableView setEditing:editing animated:animated];
}

#pragma mark 逐行的去設置,那些行編輯,哪些不行編輯
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath{
    return YES;
}

#pragma mark 設置編輯的模式
-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath{
//    return UITableViewCellEditingStyleInsert|UITableViewCellEditingStyleDelete;
    //默認是紅色 - 號,Insert是綠色加號,如果+ -取異或,變爲多選圓圈!
    return UITableViewCellEditingStyleDelete;
}
#pragma mark 設置刪除按鈕的標題
-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath{
    return @"來點我啊";
}
#pragma mark 點擊方法,實現左劃效果,而且是對應按鈕功能的點擊方法
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"編輯開始");
    //先判斷當前的編輯模式
    if (editingStyle==UITableViewCellEditingStyleDelete) {
        //先把數組裏的對象刪除掉
        [self.stuArr removeObjectAtIndex:indexPath.row];
        //第一種方式
        //[self.tableView reloadData];
        //第二種方式   系統自帶的刪除方法,可以選擇刪除動畫
        [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
    }
}

#pragma mark 移動
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath{
    //它只是視覺上移動,並沒有改變數組的順序,所以想要改變數組的順序,需要用代碼來實現
    //獲取要移動的數據
    Student *stu=[self.stuArr[sourceIndexPath.row]retain];
    //在數組裏把這個對象移除掉
    [self.stuArr removeObjectAtIndex:sourceIndexPath.row];
    [self.stuArr insertObject:stu atIndex:destinationIndexPath.row];
    [stu release];
}

//添加數組裏面需要反向添加
- (nullable NSArray<UITableViewRowAction *> *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(8_0){
    UITableViewRowAction *actionFirst=[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"刪除" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
        NSLog(@"111");
    }];
    actionFirst.backgroundColor=[UIColor redColor];
    
    UITableViewRowAction *actionTwo=[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"標爲未讀" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
        
    }];
    actionTwo.backgroundColor=[UIColor orangeColor];
    UITableViewRowAction *actionThree=[UITableViewRowAction rowActionWithStyle:UITableViewRowActionStyleDefault title:@"置頂" handler:^(UITableViewRowAction * _Nonnull action, NSIndexPath * _Nonnull indexPath) {
        
    }];
    actionThree.backgroundColor=[UIColor grayColor];
    return @[actionFirst,actionTwo,actionThree];
}

自定義刪除按鈕

#define CELL_HEIGHT (117.f)
#pragma mark 自定義編輯效果
//觸發viewDidLayoutSubviews方法的調用
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
    self.editingIndexPath = indexPath;
    [self.view setNeedsLayout];   // 觸發-(void)viewDidLayoutSubviews
}

- (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath
{
    self.editingIndexPath = nil;
}

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    
    if (self.editingIndexPath)
    {
        [self configSwipeButtons];
    }
}

- (void)configSwipeButtons
{
    // 獲取選項按鈕的reference
    if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"11.0"))   //iOS11以上系統走這個代理方法
    {
        // iOS 11層級 (Xcode 9編譯): UITableView -> UISwipeActionPullView
        for (UIView *subview in self.tableView.subviews)
        {
            if ([subview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subview.subviews count] >= 1)
            {
                UIView *swipeActionPullView = subview;
                //修改背景顏色
                swipeActionPullView.backgroundColor =  [UIColor clearColor];
                
                // 和iOS 10的按鈕順序相反
                UIButton *deleteButton = subview.subviews[0];
                
                [self configDeleteButton:deleteButton];
                
            }
        }
    }
    else
    {
        // iOS 8-10層級: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView
        HYHwLinkCell *tableCell = [self.tableView cellForRowAtIndexPath:self.editingIndexPath];
        for (UIView *subview in tableCell.subviews)
        {
            if ([subview isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")] && [subview.subviews count] >= 1)
            {
                UIView *swipeActionPullView = subview;
                //修改背景顏色
                swipeActionPullView.backgroundColor =  [UIColor clearColor];
                
                UIButton *deleteButton = subview.subviews[0];
                
                [self configDeleteButton:deleteButton];
            }
        }
    }
}

//自定義的按鈕控件
- (void)configDeleteButton:(UIButton*)deleteButton
{
    if (deleteButton)
    {
        deleteButton.frame = CGRectMake(0, 0, CELL_HEIGHT-10, CELL_HEIGHT-10);   //設置寬.高相等的frame
        [deleteButton setTitle:@"" forState:0];    //設置空不顯示,用下面的label文字
        deleteButton.layer.cornerRadius = 4;
        deleteButton.layer.masksToBounds = YES;
        deleteButton.backgroundColor = [UIColor colorWithHexValue:0xFA3232];
        
        //添加label替換默認的文字
        UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, deleteButton.width, deleteButton.height)];
        label.text = @"刪除";
        label.textColor = deleteButton.tintColor;
        label.font = [UIFont systemFontOfSize:14];
        label.textAlignment = 1;
        [deleteButton addSubview:label];
    }
}

數據刷新方法

重新刷新屏幕上的所有cell

[self.tableView reloadData];

單行刷新(刷新特定行的cell)

[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

插入特定行數的cell

- (void)addRows{
    NSMutableArray *indexPaths = [NSMutableArray array];
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:self.arr.count-1 inSection:0];
    [indexPaths addObject:indexPaths];
    //這個位置應該在修改tableView之前將數據源先進行修改,否則會崩潰. 必須向tableView的數據源數組添加一條數據
    [_tableView beginUpdates];
    [_tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];
    [_tableView endUpdates];
}

刪除特定行數的cell

- (void)removeRows{
    NSMutableArray *indexPaths = [NSMutableArray arrayWithArray:self.arr];
    [indexPaths removeObjectAtIndex:0];
    //這個位置應該在修改tableView之前將數據源先進行修改,否則會崩潰
    [_tableView beginUpdates];
    [_tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationBottom];
    [_tableView endUpdates];
}

數據刷新的原則

通過修改模型數據,來修改tableView的展示
先修改模型數據
再調用數據刷新方法
改模型 <==> 改界面
不要直接修改cell上面子控件的屬性這樣改了重用的時候不就傻逼了?

坑點

進入頁面後發現scrollView起始點不對(很奇怪的問題)

//在進入當前controller 的時候,找第一個響應的scrollView,然後給這個是scrollview 一個 inset (上, 左, 下 , 右) 偏移
self.automaticallyAdjustsScrollViewInsets = NO;

TableView 刷新後調用 reloadData 出現上移

具體參照博客 https://www.jianshu.com/p/9f9bedadf144

UITableView優化

  1. 重用池 --- 避免頻繁的對象創建
  2. 異步繪製 --- 耗時操作(如下載/圖片加載)放在子線程避免主線程卡頓
  3. 減少離屏渲染 --- 如-shadows(陰影)-shouldRasterize(光柵化)-group opacity(不透明)-設置圓角 -漸變 等
  4. 單行刷新 --- 全局刷新很消耗性能
  5. 緩存cell高度 --- 避免重複計算cell高度
  6. 預加載cell --- 絲滑般滑動體驗
    具體請看下面博客
    iOS開發思路:UITableView預加載上拉數據
    仿安卓UITableview 預加載數據

參考博客
iOS開發-UITableView進階
UITableView 的優化技巧
iOS開發——實戰UITableview深度優化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章