TL;DR:不喜欢阅读?直接跳转到 GitHub 上的示例项目:
概念描述
无论您为哪个 iOS 版本开发,下面的前 2 个步骤都适用。
1。设置和添加约束
在您的UITableViewCell 子类中,添加约束以使单元格的子视图的边缘固定到单元格的contentView 的边缘(最重要的是固定到顶部和底部边缘)。 注意:不要将子视图固定到单元格本身;仅针对单元格的contentView! 让这些子视图的内在内容大小驱动表格视图单元格内容视图的高度,方法是确保 内容压缩阻力 和 内容拥抱 每个子视图的垂直维度上的约束不会被您添加的更高优先级的约束覆盖。 (Huh? Click here.)
请记住,这个想法是让单元格的子视图垂直连接到单元格的内容视图,以便它们可以“施加压力”并使内容视图扩展以适应它们。使用带有几个子视图的示例单元格,以下是您的 一些 (不是全部!) 约束的直观图示:
您可以想象,随着更多文本添加到上面示例单元格中的多行正文标签,它需要垂直增长以适应文本,这将有效地迫使单元格增加高度。 (当然,您需要正确设置约束才能使其正常工作!)
正确设置约束绝对是使用自动布局获得动态单元高度的最困难和最重要的部分。如果你在这里犯了一个错误,它可能会阻止其他一切工作——所以慢慢来!我建议在代码中设置约束,因为您确切地知道哪些约束被添加到哪里,并且当出现问题时调试起来会容易得多。在代码中添加约束与使用布局锚点或 GitHub 上可用的出色开源 API 之一的 Interface Builder 一样简单且强大得多。
- 如果您在代码中添加约束,您应该在 UITableViewCell 子类的
updateConstraints 方法中执行此操作。请注意,updateConstraints 可能会被多次调用,因此为避免多次添加相同的约束,请确保将添加约束的代码包含在 updateConstraints 中,以检查布尔属性,例如 didSetupConstraints(您在运行一次添加约束的代码后设置为 YES)。另一方面,如果您有更新现有约束的代码(例如在某些约束上调整 constant 属性),请将其放在 updateConstraints 但在检查 didSetupConstraints 之外,以便它可以在每次方法时运行被调用。
2。确定唯一的表视图单元格重用标识符
对于单元格中的每组唯一约束,使用唯一的单元格重用标识符。换句话说,如果您的单元格具有多个唯一布局,则每个唯一布局都应收到其自己的重用标识符。 (当您的单元变体具有不同数量的子视图或子视图以不同的方式排列时,您需要使用新的重用标识符的一个很好的提示。)
例如,如果您在每个单元格中显示一封电子邮件,则可能有 4 种独特的布局:只有主题的消息、带有主题和正文的消息、带有主题和照片附件的消息以及带有主题、正文和照片附件。每个布局都有实现它所需的完全不同的约束,因此一旦初始化单元并为这些单元类型之一添加约束,单元应该获得特定于该单元类型的唯一重用标识符。这意味着当您将一个单元格出列以供重复使用时,约束已经添加并准备好使用该单元格类型。
请注意,由于内在内容大小的差异,具有相同约束(类型)的单元格可能仍然具有不同的高度!由于内容的大小不同,请勿将根本不同的布局(不同的约束)与不同的计算视图框架(从相同的约束解决)混淆。
- 不要将具有完全不同约束集的单元添加到同一个重用池(即使用相同的重用标识符),然后在每次出队后尝试删除旧约束并从头开始设置新约束。内部自动布局引擎并非旨在处理约束的大规模更改,您会看到大量的性能问题。
对于 iOS 8 - 自调整单元格
3。启用行高估计
要启用自调整大小的表格视图单元格,您必须设置表格视图的
UITableViewAutomaticDimension 的 rowHeight 属性。你还必须
为estimatedRowHeight 属性赋值。只要两者
设置好这些属性后,系统使用 Auto Layout 来计算
行的实际高度
苹果: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
这样做的目的是为表格视图提供一个临时估计/占位符,用于尚未显示在屏幕上的单元格的行高。然后,当这些单元格即将在屏幕上滚动时,将计算实际的行高。为了确定每一行的实际高度,表格视图会根据内容视图的已知固定宽度(基于表格视图的宽度减去任何其他内容,如部分索引或附件视图)以及您添加到单元格的内容视图和子视图的自动布局约束。一旦确定了这个实际的单元格高度,行的旧估计高度就会更新为新的实际高度(并且根据需要对表格视图的 contentSize/contentOffset 进行任何调整)。
一般来说,您提供的估算值不必非常准确——它仅用于正确调整表格视图中滚动指示器的大小,表格视图可以很好地调整滚动指示器以防止不正确当您在屏幕上滚动单元格时进行估计。您应该将表视图上的estimatedRowHeight 属性(在viewDidLoad 或类似中)设置为一个常数值,即“平均”行高。 只有当您的行高具有极大的可变性(例如,相差一个数量级)并且您注意到滚动指示器“跳跃”时,您才应该费心实施tableView:estimatedHeightForRowAtIndexPath: 以执行返回更准确所需的最小计算估计每一行。
为了支持 iOS 7(自己实现自动单元格大小调整)
3。进行布局传递并获取单元格高度
首先,实例化一个表格视图单元的屏幕外实例,每个重用标识符都有一个实例,它严格用于高度计算。 (离屏意味着单元格引用存储在视图控制器上的属性/ivar 中,并且永远不会从tableView:cellForRowAtIndexPath: 返回以使表格视图实际呈现在屏幕上。)接下来,必须为单元格配置确切的内容(例如文本、图像等),如果它要显示在表格视图中,它将保持。
然后,强制单元格立即布局其子视图,然后在UITableViewCell 的contentView 上使用systemLayoutSizeFittingSize: 方法找出单元格所需的高度是多少。使用UILayoutFittingCompressedSize 获得适合单元格所有内容所需的最小尺寸。然后可以从tableView:heightForRowAtIndexPath: 委托方法返回高度。
4。使用估计的行高
如果您的表视图中有超过几十行,您会发现在第一次加载表视图时执行自动布局约束求解会很快使主线程陷入困境,因为每个都调用tableView:heightForRowAtIndexPath: 并且首次加载时的每一行(以计算滚动指示器的大小)。
从 iOS 7 开始,您可以(并且绝对应该)在表格视图上使用 estimatedRowHeight 属性。这样做是为表格视图提供一个临时估计/占位符,用于尚未出现在屏幕上的单元格的行高。然后,当这些单元格即将在屏幕上滚动时,将计算实际行高(通过调用tableView:heightForRowAtIndexPath:),并将估计的高度更新为实际高度。
一般来说,您提供的估算值不必非常准确——它仅用于正确调整表格视图中滚动指示器的大小,表格视图可以很好地调整滚动指示器以防止不正确当您在屏幕上滚动单元格时进行估计。您应该将表视图上的estimatedRowHeight 属性(在viewDidLoad 或类似中)设置为一个常数值,即“平均”行高。 只有当您的行高具有极大的可变性(例如相差一个数量级)并且您注意到滚动指示器在滚动时“跳跃”时,您才应该费心实施tableView:estimatedHeightForRowAtIndexPath: 以执行返回更准确所需的最小计算估计每一行。
5。 (如果需要)添加行高缓存
如果您已完成上述所有操作,但仍然发现在 tableView:heightForRowAtIndexPath: 中进行约束求解时性能慢得令人无法接受,那么不幸的是,您需要为单元高度实现一些缓存。 (这是 Apple 工程师建议的方法。)一般的想法是让 Autolayout 引擎第一次解决约束,然后缓存该单元格的计算高度,并将缓存的值用于该单元格高度的所有未来请求。诀窍当然是确保在发生任何可能导致单元格高度变化的情况时清除单元格的缓存高度 - 主要是当单元格的内容发生变化或发生其他重要事件时(例如用户调整动态类型文本大小滑块)。
iOS 7 通用示例代码(包含大量多汁的 cmets)
- (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;
}
}
示例项目
由于表格视图单元格包含 UILabel 中的动态内容,这些项目是具有可变行高的表格视图的完整工作示例。
Xamarin (C#/.NET)
如果您使用的是 Xamarin,请查看由 @KentBoogaart 整理的 sample project。