如何在表格视图中使用UITableViewCell秒内的自动布局,让每个单元格的内容和子视图(本身/自动)确定行高,同时保持流畅的滚动性能?

推荐答案

TL;DR:你不喜欢看书吗?直接跳转到GitHub上的示例项目:

Conceptual Description

以下前两个步骤适用于您开发的iOS版本.

1. Set Up & Add Constraints

UITableViewCell子类中,添加约束,使单元的子视图的边固定到单元contentView的边(最重要的是固定到顶边和底边).NOTE: don't pin subviews to the cell itself; only to the cell's 102!通过确保每个子视图的垂直维度中的content compression resistancecontent hugging约束不会被您添加的更高优先级约束覆盖,让这些子视图的固有内容大小驱动表视图单元格内容视图的高度.(Huh? Click here.)

请记住,我们的 idea 是将单元格的子视图垂直连接到单元格的内容视图,以便它们可以"施加压力",并使内容视图扩展以适合它们.使用一个带有几个子视图的示例单元格,下面直观地说明了您的some(not all!)个约束需要看起来是什么样子:

Example illustration of constraints on a table view cell.

您可以想象,随着更多的文本添加到上面示例单元格的多行正文标签中,它将需要垂直增长以适应文本,这将有效地强制单元格增加高度.(当然,您需要获得正确的约束才能正常工作!)

使用自动布局获得动态单元高度的hardest and most important part个步骤就是正确设置约束条件.如果你在这里犯了一个错误,它可能会阻止其他一切工作——所以慢慢来!我建议您在代码中设置约束,因为您确切地知道哪些约束被添加到哪里,并且当出现问题时,调试会容易得多.在代码中添加约束与使用布局锚或GitHub上提供的一种出色的开源API的Interface Builder一样简单,功能也要强大得多.

  • 如果要在代码中添加约束,应该在UITableViewCell子类的updateConstraints方法中执行一次.请注意,updateConstraints可能会被多次调用,因此为了避免多次添加相同的约束,请确保在判断布尔属性(如didSetupConstraints)时,将约束添加代码包装在updateConstraints内(在运行约束添加代码一次后,将其设置为"是").另一方面,如果您有更新现有约束的代码(例如在某些约束上调整constant属性),请将其放在updateConstraints中,但不在didSetupConstraints的判断范围内,以便每次调用该方法时都可以运行.

2.确定唯一的表视图单元重用标识符

对于单元中的每组唯一约束,请使用唯一的单元重用标识符.换句话说,如果您的单元格具有多个唯一布局,则每个唯一布局都应该接收其自己的重用标识符.(当您的单元格变量具有不同数量的子视图,或者子视图以不同的方式排列时,您需要使用新的重用标识符.)

例如,如果您在每个单元格中显示一封邮箱,则可能有4种独特的布局:仅包含主题的邮件、包含主题和正文的邮件、包含主题和照片附件的邮件以及包含主题、正文和照片附件的邮件.每个布局都有完全不同的实现它所需的约束,因此一旦单元初始化并为这些单元类型之一添加了约束,该单元应该获得特定于该单元类型的唯一重用标识符.这意味着,当您将单元出列以供重用时,约束已经添加,并且可以用于该单元类型.

请注意,由于固有内容大小的差异,具有相同约束(类型)的单元格可能仍然具有不同的高度!不要因内容大小不同而将根本不同的布局(不同约束)与不同的计算图幅(从相同约束求解)混为一谈.

  • 不要将具有完全不同的约束集的单元添加到相同的重用池中(即使用相同的重用标识符),然后在每次出队后try 删除旧的约束并从头开始设置新的约束.内部自动布局引擎不是为处理约束中的大规模更改而设计的,您将看到大量的性能问题.

对于IOS 8-自调整大小的单元

3.启用行高预估

要启用自调整表格视图单元格大小,必须设置表格视图的

苹果:Working with Self-Sizing Table View Cells

在iOS 8中,Apple已将在iOS 8之前必须由您实现的大部分工作内部化.为了允许自调整单元格大小机制工作,您必须首先将表视图上的rowHeight属性设置为常量UITableView.automaticDimension.然后,您只需通过将表视图的estimatedRowHeight属性设置为非零值来启用行高估计,例如:

self.tableView.rowHeight = UITableView.automaticDimension;
self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is

这样做的作用是为表格视图提供尚未显示在屏幕上的单元格的行高的临时估计/占位符.然后,当这些单元格将要在屏幕上滚动时,将计算实际的行高.要确定每行的实际高度,表格视图会根据内容视图的已知固定宽度(基于表格视图的宽度,减go 节索引或附件视图等任何附加内容)和您添加到单元格的内容视图和子视图的自动布局约束,自动询问每个单元格的contentView需要多高.确定此实际单元格高度后,将使用新的实际高度更新该行的旧估计高度(并根据需要对表视图的contentSize/contentOffset进行任何调整).

一般来说,您提供的预估不必非常准确--它只用于正确调整表视图中滚动指示器的大小,并且在屏幕上滚动单元格时,表视图可以很好地调整滚动指示器,以防止不正确的预估.您应该将表视图(在viewDidLoad或类似版本中)的estimatedRowHeight属性设置为"平均"行高的常量值.Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing 102 to do the minimal calculation required to return a more accurate estimate for each row.

对于iOS 7支持(自己实现自动单元大小调整)

3. Do a Layout Pass & Get The Cell Height

首先,实例化严格用于高度计算的表视图单元格one instance for each reuse identifier的屏幕外实例.(屏幕外意味着单元格引用存储在视图控制器上的property/ivar中,并且从未从tableView:cellForRowAtIndexPath:返回,以便表格视图在屏幕上实际呈现.)接下来,必须使用在表视图中显示单元格时所包含的确切内容(例如文本、图像等)来配置单元格.

然后,强制单元立即布局其子视图,然后在UITableViewCellcontentView上使用systemLayoutSizeFittingSize:方法来确定单元所需的高度.使用UILayoutFittingCompressedSize获得容纳单元格所有内容所需的最小尺寸.然后可以从tableView:heightForRowAtIndexPath:委托方法返回高度.

4.使用估计的行高

如果您的表视图中有超过几十行,您会发现在第一次加载表视图时执行自动布局约束求解会很快使主线程停滞,因为在第一次加载时对每一行都调用tableView:heightForRowAtIndexPath:(为了计算滚动指示器的大小).

从iOS 7开始,您可以(而且绝对应该)在表视图上使用estimatedRowHeight属性.这样做的目的是为表格视图提供一个临时估计/占位符,用于显示尚未显示在屏幕上的单元格的行高.然后,当这些单元格即将在屏幕上滚动时,将计算实际行高(通过调用tableView:heightForRowAtIndexPath:),并用实际行高更新估计的行高.

一般来说,您提供的预估不必非常准确--它只用于正确调整表视图中滚动指示器的大小,并且在屏幕上滚动单元格时,表视图可以很好地调整滚动指示器,以防止不正确的预估.您应该将表视图(在viewDidLoad或类似版本中)的estimatedRowHeight属性设置为"平均"行高的常量值.Only if your row heights have extreme variability (e.g. differ by an order of magnitude) and you notice the scroll indicator "jumping" as you scroll should you bother implementing 102 to do the minimal calculation required to return a more accurate estimate for each row.

5.(如果需要)添加行高缓存

如果您已经完成了以上所有操作,但仍然发现在以tableView:heightForRowAtIndexPath:进行约束求解时性能慢得令人无法接受,那么很遗憾,您将需要实现一些针对单元格高度的缓存.(这是苹果工程师建议的方法.)一般的 idea 是让Autolayout引擎第一次解决约束,然后缓存该单元格的计算高度,并将缓存的值用于以后对该单元格高度的所有请求.当然,诀窍是确保在发生任何可能导致单元格高度更改的事情时清除该单元格的缓存高度--主要是在该单元格的内容发生更改或发生其他重要事件时(如用户调整dynamic Type文本大小滑块).

iOS 7通用示例代码(有很多有趣的注释)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path, depending on the particular layout required (you may have
    // just one, or may have many).
    NSString *reuseIdentifier = ...;

    // Dequeue a cell for the reuse identifier.
    // Note that this method will init and return a new cell if there isn't
    // one available in the reuse pool, so either way after this line of 
    // code you will have a cell with the correct constraints ready to go.
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier];
         
    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...
    
    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // If you are using multi-line UILabels, don't forget that the 
    // preferredMaxLayoutWidth needs to be set correctly. Do it at this 
    // point if you are NOT doing it within the UITableViewCell subclass 
    // -[layoutSubviews] method. For example: 
    // cell.multiLineLabel.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds);
    
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Determine which reuse identifier should be used for the cell at this 
    // index path.
    NSString *reuseIdentifier = ...;

    // Use a dictionary of offscreen cells to get a cell for the reuse 
    // identifier, creating a cell and storing it in the dictionary if one 
    // hasn't already been added for the reuse identifier. WARNING: Don't 
    // call the table view's dequeueReusableCellWithIdentifier: method here 
    // because this will result in a memory leak as the cell is created but 
    // never returned from the tableView:cellForRowAtIndexPath: method!
    UITableViewCell *cell = [self.offscreenCells objectForKey:reuseIdentifier];
    if (!cell) {
        cell = [[YourTableViewCellClass alloc] init];
        [self.offscreenCells setObject:cell forKey:reuseIdentifier];
    }
    
    // Configure the cell with content for the given indexPath, for example:
    // cell.textLabel.text = someTextForThisCell;
    // ...
    
    // Make sure the constraints have been set up for this cell, since it 
    // may have just been created from scratch. Use the following lines, 
    // assuming you are setting up constraints from within the cell's 
    // updateConstraints method:
    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Set the width of the cell to match the width of the table view. This
    // is important so that we'll get the correct cell height for different
    // table view widths if the cell's height depends on its width (due to 
    // multi-line UILabels word wrapping, etc). We don't need to do this 
    // above in -[tableView:cellForRowAtIndexPath] because it happens 
    // automatically when the cell is used in the table view. Also note, 
    // the final width of the cell may not be the width of the table view in
    // some cases, for example when a section index is displayed along 
    // the right side of the table view. You must account for the reduced 
    // cell width.
    cell.bounds = CGRectMake(0.0, 0.0, CGRectGetWidth(tableView.bounds), CGRectGetHeight(cell.bounds));

    // Do the layout pass on the cell, which will calculate the frames for 
    // all the views based on the constraints. (Note that you must set the 
    // preferredMaxLayoutWidth on multiline UILabels inside the 
    // -[layoutSubviews] method of the UITableViewCell subclass, or do it 
    // manually at this point before the below 2 lines!)
    [cell setNeedsLayout];
    [cell layoutIfNeeded];

    // Get the actual height required for the cell's contentView
    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    // Add an extra point to the height to account for the cell separator, 
    // which is added between the bottom of the cell's contentView and the 
    // bottom of the table view cell.
    height += 1.0;

    return height;
}

// NOTE: Set the table view's estimatedRowHeight property instead of 
// implementing the below method, UNLESS you have extreme variability in 
// your row heights and you notice the scroll indicator "jumping" 
// as you scroll.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Do the minimal calculations required to be able to return an 
    // estimated row height that's within an order of magnitude of the 
    // actual height. For example:
    if ([self isTallCellAtIndexPath:indexPath]) {
        return 350.0;
    } else {
        return 40.0;
    }
}

Sample Projects

由于表格视图单元格包含UILabels中的动态内容,这些项目都是具有可变行高的表格视图的完整工作示例.

Xamarin(C#/.NET)

如果你用的是Xamarin,看看这sample project个加在一起的@KentBoogaart.

Ios相关问答推荐

在SwiftData中从@ Query创建可排序、有序和分组的数据

iOS中的分段拾取器—手柄点击已 Select 的项目

阿拉伯文变音符号(Harakat)在文本对齐的UILabel中未对齐

我应该使用哪个UIButton发送事件在SWIFT上刷卡?

为什么下面的代码没有在主线程上运行?

Flutter应用内购买无法恢复购买

将 Riverpod 从 StateNotifier 修复为 NotifierProvider 以及应用程序生命周期监控

如何链式设置 AttributeContainer 的 UIKit 属性?

Swift Combine和iOS版本限制?

部分保留 UITextField 中的占位符

var name: String 时 print(nil) 和 print(name) 的区别? = 无

DllImport with .a file for iOS in MAUI

发生异常:需要 issuerId

SwiftUI 我自己的渲染函数的返回类型是什么?

在 tableview 中下载图像缩略图时如何提高滚动性能

使用导航控制器在prepareForSegue中设置数据

是否可以使用 sharedHTTPCookieStorage 为 UIWebView 手动设置 cookie?

针对 Xcode 中的 iPad 选项

UITableView:从空白部分隐藏标题

快速从 NSTimeInterval 转换为小时、分钟、秒、毫秒