0
点赞
收藏
分享

微信扫一扫

UITableViewCell计算行高的几种方式

楚木巽 2021-09-22 阅读 108

原文 : 与佳期的个人博客(gonghonglou.com)

当我还是小白时(比现在更白的时候)对于 UITableViewCell 的行高问题还是比较头疼的,当然这个算是 iOS 开发中相当基础的内容了,但是当时的我就想过,将 UITableViewCell 的行高问题解决了一定写一篇总结博客,所以就有了这篇博客。尽管拖了这。么。久。。。

iOS 的界面中,UITableView 应该是用的最多的控件之一了吧:微信列表、聊天记录、朋友圈、微博 time line。。。哪哪离不开 UITableView。而 UITableView 则是由 UITableViewCell 组成的,这些 cell 有的行高是固定的,大部分则需要根据内容反计算行高来展示。本篇博客则来介绍 UITableViewCell 计算行高的几种方式。

固定行高

有相当一部分 UITableView 的行高是固定的,这种 cell 在代码书写和代码性能上相比而言就简单了许多,有以下两种方式设定行高:

统一设定

self.tableView.rowHeight = 44; // 系统自带的 cell 的行高大概就是 44
  • 优点:这种方式最为简单。
  • 缺点:相对的对 tableView 的可控性也最弱,它会将 tableView 所有的 cell 高度统一设置为 44。

通过代理设定

通过实现 UITableViewDelegate 方法,同样可以控制 tableView 的行高。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 1.
    // return 44;
    
    // 2.
    // if (tableView == self.tableView1) {
    //  return 44;
    // } else {
    //  return 88;
    // }
    
    // 3.
    if (indexPath.row == 1) {
        return 44;
    } else {
        return 88;
    }
}
  • 优点:采用这种实现代理的方法可以对不同的 tableView 、tableView 中不同的 section、row,进行判断分别设置,可控性更强。
  • 缺点:因为在每次展示 cell 时都会调用了一次该代理方法,所以较于第一种方法一些性能损耗。

这个缺点在苹果介绍 rowHeight 属性的文档里也有指明:

所以,如果你的 tableView 的行高是统一的、固定的,那么最好采用第一种方法,直接设置 rowHeight。

不定行高

固定行高的 tableView 已经算是小儿科了,但是也属于 UITableViewCell 行高的范畴,所以还是简单提了一下。
那么,当 tableView 的行高不固定时,有以下几种计算方式。

估算行高

其实,UITableView 的 rowHeight 可以设置为 UITableViewAutomaticDimension,顾名思义,cell 可以设置为自动计算行高。然而,仅仅将 rowHeight 设置为 UITableViewAutomaticDimension 对自计算行高是不起效的,这时候就需要另一个属性:estimatedRowHeight

iOS8 苹果推出了 self-sizing 的概念。UITableView 在 iOS7 就增加了一个属性: estimatedRowHeight ,苹果是这样描述的:

你可以使用这个属性来给 cell 估算行高,但是默认值为 0,不会进行估算。
所以如果想开启估算行高的话,必须设置 estimatedRowHeight,如:

self.tableView.estimatedRowHeight = 60;
self.tableView.rowHeight = UITableViewAutomaticDimension;

将 estimatedRowHeight 设置为一个大概的估计行高值即可,没有严格的限制。比如你的 cell 高度大概在 50 到 100 之间,那么你可以将 estimatedRowHeight 设置为 75;
rowHeight 的默认值为 UITableViewAutomaticDimension,所以第二行可以省略。

然后就是对你的 cell 进行布局设置,这里以 Masonry 为例(AutoLayout 的话,这个框架大家应该都在用吧?~)
伪代码如下:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        __weak __typeof(self)weakSelf = self;
        
        UILabel *label1 = [UILabel new];
        [self.contentView addSubview:label1];
        [label1 mas_makeConstraints:^(MASConstraintMaker *make) {
            __strong __typeof(weakSelf)strongSelf = weakSelf;
            make.top.equalTo(strongSelf.contentView).with.offset(10);
            make.left.equalTo(strongSelf.contentView).with.offset(10);
            make.right.equalTo(strongSelf.contentView).with.offset(-10);
            make.height.mas_equalTo(22); // ①
        }];
        
        UILabel *label2 = [UILabel new];
        [label2 setNumberOfLines:0];
        [self.contentView addSubview:label2];
        [label2 mas_makeConstraints:^(MASConstraintMaker *make) {
            __strong __typeof(weakSelf)strongSelf = weakSelf;
            make.top.equalTo(label1.mas_bottom).with.offset(10);
            make.left.equalTo(label1);
            make.right.equalTo(label1);
            make.bottom.equalTo(strongSelf.contentView.mas_bottom).with.offset(-10); // ②
        }];
    }
    return self;
}

注释①:该条约束可以注释掉,然后将 label1 设置为 [label1 setNumberOfLines:0]; 这样 label1 就可以根据内容自动计算高度。
注释②:必须设置该条约束。为了确定 cell 的高度,最接近 cell bottom 的控件需要设置一个距离 cell bottom 的约束。

布局结果大概是这样的效果:

  • 优点:代码书写简单,页面布局快捷。在 cell 的 initWithStyle: 方法里就已经新建各个控件并将位置设置好,并且不需要单独计算 cell 的高度。
  • 缺点:效率低,稍微复杂些的页面就能感觉到 tableView 滑动时的掉帧。

代理计算行高

首先来执行一波伪代码,用打印来观察各个代理方法的执行顺序:

#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    NSLog(@"---方法 : numberOfRowsInSection: ---section : %ld", (long)section);
    
    return 10;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"---方法 : cellForRowAtIndexPath: ---row : %ld", (long)indexPath.row);
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    return cell;
}

#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSLog(@"---方法 : heightForRowAtIndexPath: ---row : %ld", (long)indexPath.row);

    return cellHight;
}

控制台打印结果如下:

①:首先执行 numberOfRowsInSection: 方法,返回 cell 个数为 10。
②:其次执行的就是 heightForRowAtIndexPath: 方法,如上图,此时执行该方法会将所有 cell 的高度全部返回。
③④⑤:这时候就开始执行 cellForRowAtIndexPath: 方法,因为当前页面只能布局 3 条 cell,所以该方法会被执行三次。并且,执行一次 cellForRowAtIndexPath: 方法紧接着就会执行一次 heightForRowAtIndexPath: 方法返回 cell 高度。

因此,当我们从网络或者本地缓存中获取到所需数据( array )后,可以直接执行代码:

[self.tableView reloadData];

然后就会调用 cellForRowAtIndexPath: 方法和 heightForRowAtIndexPath: 方法。

cell 的 initWithStyle: 方法:

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {        
        UILabel *label1 = [UILabel new];
        [self.contentView addSubview:label1];
        
        UILabel *label2 = [UILabel new];
        [label2 setNumberOfLines:0];
        [self.contentView addSubview:label2];
    }
    return self;
}

我们可以在 cellForRowAtIndexPath: 方法进行 cell 布局,如:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }

    // 内容
    cell.label1.text = @"...";
    cell.label2.text = @"...";
    
    // 布局
    CGFloat viewWidth = [UIScreen mainScreen].bounds.size.width;
    cell.label1.fream = CGRectMake(10, 10, viewWidth-20, 22);
    
    CGFloat label2Height = [text boundingRectWithSize:CGSizeMake(viewWidth-20, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size.height;
    cell.label2.fream = CGRectMake(10, 10, viewWidth-20, label2Height);
    return cell;
}

布局结果大概是这样的效果:

然后在 heightForRowAtIndexPath: 方法里根据 array 数据计算 cell 的高度,如:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat viewWidth = [UIScreen mainScreen].bounds.size.width;
    CGFloat label2Height = [text boundingRectWithSize:CGSizeMake(viewWidth-20, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size.height;
    return 10 + 22 + 10 + label2Height + 10;
}
  • 优点:通过 feram 的方式能够提高布局效率
  • 缺点:因为每次 cell 将要出现时都去执行一次 heightForRowAtIndexPath: 所以效率还是不够高,而且行高没有得到缓存,造成大量不必要的计算上的浪费。所以就有了下边这种方法。

提前计算行高

这种方式是出自 MVVM 的产物,因为最近都在使用 MVVM 框架,所有的计算都放到了 viewModel 里,所以在 viewModel 从网络或者本地缓存拿到数据后接着就会处理,将原始数据处理封装成 cell 的 VO(view object) 类对象,该 VO 类里包含着 cell 所需要的展示内容和尺寸,如:

@interface TableViewCellVO : NSObject
// 数据
@property (nonatomic, copy) NSString *label1Text;
@property (nonatomic, copy) NSString *label2Text;
// 尺寸
@property (nonatomic, assign) CGRect label1Fream;
@property (nonatomic, assign) CGRect label2Fream;
// 行高
@property (nonatomic, assign) CGFloat cellHeight;
@end

然后将处理好的 cellVOArray 传给 UITableView 的数据源和代理,在 cellForRowAtIndexPath: 方法直接赋值:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
    }
    TableViewCellVO *cellVO = cellVOArray[indexPath.row];
    // 数据
    cell.label1.text = cellVO.label1Text;
    cell.label2.text = cellVO.label2Text;
    // 布局
    cell.label1.fream = cellVO.label1Fream;
    cell.label2.fream = cellVO.label2Fream;
    return cell;
}

heightForRowAtIndexPath: 方法里直接返回行高:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCellVO *cellVO = cellVOArray[indexPath.row];
    return cellVO.cellHeight
}
  • 优点:行高与 cell 内的各控件尺寸都提前计算好,在执行 cellForRowAtIndexPath: 方法和 heightForRowAtIndexPath: 方法时比较快,且行高得到缓存,避免了冗余的计算。
  • 缺点:在从网络或者本地缓存拿到数据之后,执行 [self.tableView reloadData]; 之前需要花费时间处理数据及计算 fream。这里需要谨慎处理数据,可以采用多线程等技术缩短数据处理的时间。

因为在数据处理上所花费的时间要远小于页面滚动时所消耗的时间。凡是涉及页面的操作都是相当耗费时间的,相比而言对于 cpu 在处理数据上的时间就可以忽略不计了,毕竟我们所要处理的数据都不会太大,如果数据过多的话可以做分次获取处理(下拉刷新操作),所以这里的缺点相对于上一种方法还是可以忍受的。

XIB 方式处理 UITableViewCell 的行高问题

对于纯 Code 和 SB 方式的页面布局问题业界已经相爱相杀了很久,各有各的道理。我的观点是:哪种布局方式适合自己就好了,看个人喜好。相比于 SB 的“所见即所得”,快速布局等,我个人更喜欢纯 Code 的方式,对代码的可控性更强(亦不排除我个人是比较喜欢 Coding 的因素)。
不过,如果你是偏好 SB 的布局方式,采用 XIB 方式处理 UITableViewCell,那么你可以看看sunnyxx 的 UITableView-FDTemplateLayoutCell(点击可查看GitHub)这种解决方案。
而且,本文的前一部分也参考了 sunnyxx 的这篇博客 优化UITableViewCell高度计算的那些事
,推荐阅读。

后记

  • 小白出手,请多指教。如言有误,还望斧正!

  • 转载请保留原文地址:http://gonghonglou.com/2017/07/05/cell-height

举报

相关推荐

0 条评论