在UITableView中使用自动布局实现动态单元格布局和可变行高

1611

如何在表视图的UITableViewCell中使用自动布局(Auto Layout)以使每个单元格的内容和子视图决定行高(自身/自动),同时保持平滑的滚动性能?


这是Swift 2.3的示例代码 https://github.com/dpakthakur/DynamicCellHeight - Deepak Thakur
要理解关于多行UILabel的大部分答案,您需要完全了解contentSizepreferredMaxLayoutWidth的工作原理。请参见此处。话虽如此,如果您正确设置约束,则不需要preferredMaxLayoutWidth,事实上它可能会产生意外结果。 - mfaani
2020年的重要提示,针对您的UILabel。假设它通过一个约束附加在左侧。您还必须使用“大于或等于零”的约束将其附加到右侧。 - Fattie
29个回答

2480
TL;DR: 不喜欢阅读?直接跳转到GitHub上的示例项目:

概念描述

以下前两步适用于您开发的任何iOS版本。

1. 设置和添加约束

在你的UITableViewCell子类中,添加约束条件,使单元格的子视图的边缘固定到单元格的contentView的边缘上(最重要的是顶部和底部边缘)。注意:不要将子视图固定到单元格本身;只能固定到单元格的contentView让这些子视图的内在内容大小驱动表格视图单元格的内容视图的高度,确保每个子视图在垂直方向上的内容压缩阻力内容吸附约束没有被您添加的更高优先级的约束条件覆盖。(什么?点击这里。
记住,想法是将单元格的子视图垂直连接到单元格的内容视图,以便它们可以"施加压力"并使内容视图扩展以适应它们。使用一个带有几个子视图的示例单元格,下面是一种可视化说明,展示了一些(不是全部!)约束条件需要看起来像什么:

Example illustration of constraints on a table view cell.

你可以想象,随着在上述示例单元格中添加更多文本到多行正文标签,它将需要垂直增长以适应文本,这将有效地强制单元格增高。(当然,您需要正确设置约束条件才能使其正常工作!)
确保你的约束条件正确是让自动布局实现动态单元格高度最难且最重要的部分。如果你在这里犯了一个错误,它可能会阻止其他所有东西正常工作--所以要耐心!我建议使用代码设置约束条件,因为你知道哪些约束条件在哪里被添加,当出现问题时,这样调试起来要容易得多。使用布局锚点或GitHub上可用的一些出色的开源API之一,在代码中添加约束完全可以像使用界面构建器一样简单,但功能更加强大。
如果你要在代码中添加约束条件,应该在你的UITableViewCell子类的updateConstraints方法内执行一次。请注意,updateConstraints可能会被调用多次,为了避免重复添加相同的约束条件,请确保将你的约束条件添加代码包装在检查布尔属性(例如didSetupConstraints)的updateConstraints中,设定为YES,表示您已经运行过一次约束添加代码。另一方面,如果你有更新现有约束条件的代码(例如调整某些约束条件的constant属性),将其放置在updateConstraints之外,但不放在对didSetupConstraints的检查中,以便每次调用该方法时都可以运行。
对于每个单独的约束条件集,在cell中使用唯一的重用标识符。换句话说,如果你的cell有多个唯一的布局,则每个唯一的布局应该拥有自己的重用标识符。(需要使用新的重用标识符的一个好提示是当您的cell变体拥有不同数量的子视图或子视图以不同的方式排列时。)
例如,如果您在每个单元格中显示一封电子邮件,则可能有4种唯一的布局:仅具有主题的邮件、具有主题和正文的邮件、具有主题和照片附件的邮件以及具有主题、正文和照片附件的邮件。每个布局都需要完全不同的约束条件才能实现,因此一旦单元格初始化并添加了这些单元格类型之一的约束条件,该单元格应该获得特定于该单元格类型的唯一重用标识符。这意味着当您为重用而出列一个单元格时,约束已经被添加并准备好使用该单元格类型。
请注意,由于内在内容大小的差异,具有相同约束(类型)的单元格仍可能具有不同的高度!不要将根本不同的布局(不同的约束)与不同的计算视图框架(由于内容的不同大小而从相同的约束解决)混淆。
  • 不要将具有完全不同约束集的单元格添加到同一个重用池中(即使用相同的重用标识符),然后尝试在每次出列后删除旧约束并从头设置新约束。内部自动布局引擎不适用于处理约束的大规模更改,您将看到严重的性能问题。

对于iOS 8-自尺寸单元格

3. 启用行高度估算

为了启用自动调整大小的表格视图单元格,您必须将表格视图的rowHeight属性设置为UITableViewAutomaticDimension。您还必须为estimatedRowHeight属性赋值。一旦这两个属性都设置好了,系统就使用自动布局来计算行的实际高度。
随着 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

这段代码的作用是为表格视图提供一种临时的估算/占位符,用于尚未在屏幕上显示的单元格行高度。然后,当这些单元格即将滚动到屏幕上时,实际的行高度将被计算出来。为了确定每行的实际高度,表格视图会自动询问每个单元格其contentView的高度,该高度基于内容视图的已知固定宽度(基于表格视图的宽度,减去任何其他内容,如部分索引或附件视图)和您添加到单元格的内容视图和子视图的自动布局约束。一旦确定了实际单元格高度,旧的行高估计值就会更新为新的实际高度(并根据需要进行表格视图的contentSize/contentOffset调整)。
总的来说,你提供的估计值不必非常精确 -- 它仅用于正确地调整表视图中的滚动指示器,而表视图会在你滚动屏幕上的单元格时自动调整不正确的估计值。你应该在表视图上设置 estimatedRowHeight 属性(在 viewDidLoad 或类似方法中)为一个常量值,即"平均"行高。只有当你的行高具有极端的变化(例如相差一到两个数量级),并且你注意到在滚动时滚动指示器会"跳跃"时,你才需要实现 tableView:estimatedHeightForRowAtIndexPath: 来进行最小限度的计算以返回每行更准确的估计值。

对于支持 iOS 7 的版本(手动实现自动单元格大小)

3. 进行布局并获取单元格高度

首先,实例化一个离屏的表格视图单元格实例,每个重用标识符使用一个实例,该实例严格用于高度计算。(离屏意味着单元格引用被存储在视图控制器的属性/ivar中,并且从tableView:cellForRowAtIndexPath:方法中不返回到表格视图以实际渲染在屏幕上。)接下来,必须使用与它在表格视图中显示时相同的内容(例如文本、图像等)配置单元格。
然后,强制单元格立即布局其子视图,然后在UITableViewCellcontentView上使用systemLayoutSizeFittingSize:方法,找出所需单元格高度。使用UILayoutFittingCompressedSize获取适合所有单元格内容的最小大小。然后可以从tableView:heightForRowAtIndexPath:委托方法返回高度。

4. 使用预估行高

如果您的表视图中有超过几十行,您会发现在首次加载表视图时,执行自动布局约束求解可能会迅速拖慢主线程,因为会在每一行上调用tableView:heightForRowAtIndexPath:(以计算滚动指示器的大小)。
从iOS 7开始,您可以(而且绝对应该)使用表视图上的estimatedRowHeight属性。这将为表视图提供一个临时的估计/占位符,用于尚未在屏幕上的单元格的行高。然后,当这些单元格即将滚动到屏幕上时,将计算实际的行高(通过调用tableView:heightForRowAtIndexPath:),并更新估计高度为实际高度。
一般来说,您提供的估计值不必非常准确 - 它仅用于正确地调整表视图中的滚动指示器,并且表视图会在您滚动屏幕上的单元格时根据不正确的估计进行良好的滚动指示器调整。您应该在表视图上设置estimatedRowHeight属性(在viewDidLoad或类似方法中)为一个常量值,即“平均”行高度。只有当您的行高具有极端的变化(例如相差一个数量级),并且您注意到滚动指示器在滚动时“跳跃”,您才应该实现tableView:estimatedHeightForRowAtIndexPath:以执行所需的最小计算,以返回每行更准确的估计值。

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

如果您已经完成了上述所有步骤,但在tableView:heightForRowAtIndexPath:中进行约束求解时仍然发现性能不可接受地慢,则不幸的是,您需要为单元格高度实现一些缓存。(这是苹果工程师建议的方法。)总体思路是让自动布局引擎第一次解决约束,然后缓存计算出的该单元格的高度,并将缓存值用于该单元格的所有未来高度请求。当然,诀窍在于确保在发生任何可能导致单元格高度更改的事件(主要是单元格内容更改或其他重要事件发生时,如用户调整动态类型文本大小滑块时)时清除单元格的缓存高度。

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;
    }
}

示例项目

这些项目是包含动态内容的 UILabel 的表视图的可变行高度的完整工作示例。

Xamarin(C#/.NET)

如果您正在使用 Xamarin,请查看由 @KentBoogaart 组成的 示例项目


1
虽然这个程序目前运行良好,但我发现它稍微低估了所需的尺寸(可能是由于各种舍入问题),为了使所有文本都适合标签中,我必须在最终高度上添加一些点。 - DBD
5
@Alex311 很有趣,感谢您提供这个例子。我在我的端上进行了一些测试,并在这里撰写了一些评论:https://github.com/Alex311/TableCellWithAutoLayout/commit/bde387b27e33605eeac3465475d2f2ff9775f163#commitcomment-4633188 - smileyborg
4
我强烈建议为每个单元重用标识符类型进行缓存。每次你为高度出列时,都会从队列中取出一个未添加回去的单元格。缓存可以显著降低表格单元初始化器被调用的次数。由于某种原因,当我没有进行缓存时,随着滚动,所使用的内存量不断增加。 - Alex311
1
除了单元格的宽度没有填满整个表视图的宽度之外,其他都没问题。我通过在单元格中添加一个带有约束的空白 UIView 来解决这个问题,其中约束设置为 trailing 和 leading。然后,在 heightForRowAtIndexPath 中,通过编程方式添加一个宽度约束,将其设置为根控制器的 view.bounds.width(或更改任何第二次调用的 constraint.constant)。 - Brian Birtle
2
这个答案发布后,iOS8的实现仍然推荐用于iOS9iOS10吗?还是有新的方法? - koen
显示剩余111条评论

198

iOS 8及以上版本非常简单:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableView.automaticDimension
}

或者
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableView.automaticDimension
}

但是对于iOS 7而言,关键是在自动布局之后计算高度:

func calculateHeightForConfiguredSizingCell(cell: GSTableViewCell) -> CGFloat {
    cell.setNeedsLayout()
    cell.layoutIfNeeded()
    let height = cell.contentView.systemLayoutSizeFittingSize(UILayoutFittingExpandedSize).height + 1.0
    return height
}

重要提示

  • 如果标签有多行,请不要忘记将numberOfLines设置为0

  • 不要忘记label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

完整的示例代码在这里


9
我认为在 iOS 8 的情况下不需要实现 heightForRowAtIndexPath 函数。 - jpulikkottil
正如@eddwinpaz在另一个答案中指出的那样,重要的是用于锁定行高度的约束不包括边距。 - Richard
1
+1 表示“如果有多行标签,请不要忘记将 numberOfLines 设置为 0”,这会导致我的单元格大小不具有动态性。 - Ian-Fogelman
虽然可能会被称为控制狂,但即使在 iOS 11 的时代,我仍然不喜欢使用 UITableViewAutomaticDimension。过去曾经有一些不好的经历。因此,我通常使用这里列出的 iOS7 解决方案。William 在这里提醒我们不要忘记 label.preferredMaxLayoutWidth,这点对我很有帮助。 - Brian Sachetta
当我们滚动时,标签开始以多行显示,并且行高也不增加。 - Karanveer Singh
@KaranveerSingh 我认为你的tableViewCell有一些限制,一个提示是你必须确保可以推断(计算)单元格的高度。比如,如果你没有设置标签的尾部约束,对于标签来说没有问题,但是对于单元格的布局来说就有问题了。 - William Hu

99

可变高度的UITableViewCell的Swift示例

更新至Swift 3

William Hu的Swift答案很好,但对于第一次学习某个东西时,有些简单而详细的步骤会对我有所帮助。下面的示例是我在学习如何制作具有可变单元格高度的UITableView时创建的测试项目。我基于这个Swift的基本UITableView示例进行了修改。

最终项目应如下所示:

enter image description here

创建新项目

可以只是一个Single View Application。

添加代码

向项目中添加一个新的Swift文件。将其命名为MyCustomCell。此类将保存你在故事板中为单元格添加的视图的输出口。在这个基本示例中,我们每个单元格只有一个标签。

import UIKit
class MyCustomCell: UITableViewCell {
    @IBOutlet weak var myCellLabel: UILabel!
}

我们稍后会连接这个插座。
打开ViewController.swift文件,确保您拥有以下内容:
import UIKit
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {

    // These strings will be the data for the table view cells
    let animals: [String] = [
        "Ten horses:  horse horse horse horse horse horse horse horse horse horse ",
        "Three cows:  cow, cow, cow",
        "One camel:  camel",
        "Ninety-nine sheep:  sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep baaaa sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep sheep",
        "Thirty goats:  goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat goat "]

    // Don't forget to enter this in IB also
    let cellReuseIdentifier = "cell"

    @IBOutlet var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        // delegate and data source
        tableView.delegate = self
        tableView.dataSource = self

        // Along with auto layout, these are the keys for enabling variable cell height
        tableView.estimatedRowHeight = 44.0
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    // number of rows in table view
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.animals.count
    }

    // create a cell for each table view row
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell:MyCustomCell = self.tableView.dequeueReusableCell(withIdentifier: cellReuseIdentifier) as! MyCustomCell
        cell.myCellLabel.text = self.animals[indexPath.row]
        return cell
    }

    // method to run when table view cell is tapped
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("You tapped cell number \(indexPath.row).")
    }
}

重要提示:


这里需要注意的是:
  • It is the following two lines of code (along with auto layout) that make the variable cell height possible:

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    

设置Storyboard

在视图控制器中添加一个表格视图(Table View),并使用自动布局将其固定在四个边缘。然后将表格视图单元格(Table View Cell)拖到表格视图上。在原型单元格(Prototype cell)上,拖动一个标签(Label)。使用自动布局将标签固定在表格视图单元格的内容视图(content view)的四个边缘。

enter image description here

重要提示:

  • 自动布局与我上面提到的两行重要代码一起使用。如果您不使用自动布局,它将无法正常工作。

其他IB设置

自定义类名和标识符

选择表格视图单元格(Table View Cell),将自定义类设置为MyCustomCell(我们添加的Swift文件中的类名)。同时将标识符(Identifier)设置为cell(与上面代码中使用的cellReuseIdentifier相同的字符串)。

enter image description here

标签零行

在标签中将行数设置为0。这意味着多行,并允许标签根据其内容自动调整大小。

enter image description here

连接插座(Outlets)

  • 从Storyboard中的表格视图(Table View)向ViewController代码中的tableView变量进行控制拖动。
  • 对于原型单元格中的标签(Label),也进行相同的操作,将其拖到MyCustomCell类中的myCellLabel变量上。

完成

现在您应该能够运行项目并获得可变高度的单元格。

注释

  • This example only works for iOS 8 and after. If you are still needing to support iOS 7 then this won't work for you.
  • Your own custom cells in your future projects will probably have more than a single label. Make sure that you get everything pinned right so that auto layout can determine the correct height to use. You may also have to use vertical compression resistance and hugging. See this article for more about that.
  • If you are not pinning the leading and trailing (left and right) edges, you may also need to set the label's preferredMaxLayoutWidth so that it knows when to line wrap. For example, if you had added a Center Horizontally constraint to the label in the project above rather than pin the leading and trailing edges, then you would need to add this line to the tableView:cellForRowAtIndexPath method:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    

另请参阅


preferredMaxLayoutWidth 设定为单元格的 contentSize 不是更好吗?这样,如果你有一个 accessoryView 或者它被滑动编辑,那么它仍然会被考虑在内。 - mfaani
@Honey,你很有可能是对的。我已经有一年没有跟进iOS的最新动态了,现在回答你可能不太准确。 - Suragch

65

我使用类别将@smileyborg的iOS7解决方案包装起来

我决定将@smileyborg的聪明解决方案包装到UICollectionViewCell+AutoLayoutDynamicHeightCalculation类别中。

该类别还纠正了@wildmonkey答案中概述的问题(从nib加载单元格和systemLayoutSizeFittingSize:返回CGRectZero)。

它不考虑任何缓存,但目前符合我的需求。随意复制、粘贴并对其进行修改。

UICollectionViewCell+AutoLayoutDynamicHeightCalculation.h

#import <UIKit/UIKit.h>

typedef void (^UICollectionViewCellAutoLayoutRenderBlock)(void);

/**
 *  A category on UICollectionViewCell to aid calculating dynamic heights based on AutoLayout contraints.
 *
 *  Many thanks to @smileyborg and @wildmonkey
 *
 *  @see stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights
 */
@interface UICollectionViewCell (AutoLayoutDynamicHeightCalculation)

/**
 *  Grab an instance of the receiving type to use in order to calculate AutoLayout contraint driven dynamic height. The method pulls the cell from a nib file and moves any Interface Builder defined contrainsts to the content view.
 *
 *  @param name Name of the nib file.
 *
 *  @return collection view cell for using to calculate content based height
 */
+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name;

/**
 *  Returns the height of the receiver after rendering with your model data and applying an AutoLayout pass
 *
 *  @param block Render the model data to your UI elements in this block
 *
 *  @return Calculated constraint derived height
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width;

/**
 *  Directly calls `heightAfterAutoLayoutPassAndRenderingWithBlock:collectionViewWidth` assuming a collection view width spanning the [UIScreen mainScreen] bounds
 */
- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block;

@end

UICollectionViewCell+AutoLayoutDynamicHeightCalculation.m

#import "UICollectionViewCell+AutoLayout.h"

@implementation UICollectionViewCell (AutoLayout)

#pragma mark Dummy Cell Generator

+ (instancetype)heightCalculationCellFromNibWithName:(NSString *)name
{
    UICollectionViewCell *heightCalculationCell = [[[NSBundle mainBundle] loadNibNamed:name owner:self options:nil] lastObject];
    [heightCalculationCell moveInterfaceBuilderLayoutConstraintsToContentView];
    return heightCalculationCell;
}

#pragma mark Moving Constraints

- (void)moveInterfaceBuilderLayoutConstraintsToContentView
{
    [self.constraints enumerateObjectsUsingBlock:^(NSLayoutConstraint *constraint, NSUInteger idx, BOOL *stop) {
        [self removeConstraint:constraint];
        id firstItem = constraint.firstItem == self ? self.contentView : constraint.firstItem;
        id secondItem = constraint.secondItem == self ? self.contentView : constraint.secondItem;
        [self.contentView addConstraint:[NSLayoutConstraint constraintWithItem:firstItem
                                                                     attribute:constraint.firstAttribute
                                                                     relatedBy:constraint.relation
                                                                        toItem:secondItem
                                                                     attribute:constraint.secondAttribute
                                                                    multiplier:constraint.multiplier
                                                                      constant:constraint.constant]];
    }];
}

#pragma mark Height

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block
{
    return [self heightAfterAutoLayoutPassAndRenderingWithBlock:block
                                            collectionViewWidth:CGRectGetWidth([[UIScreen mainScreen] bounds])];
}

- (CGFloat)heightAfterAutoLayoutPassAndRenderingWithBlock:(UICollectionViewCellAutoLayoutRenderBlock)block collectionViewWidth:(CGFloat)width
{
    NSParameterAssert(block);

    block();

    [self setNeedsUpdateConstraints];
    [self updateConstraintsIfNeeded];

    self.bounds = CGRectMake(0.0f, 0.0f, width, CGRectGetHeight(self.bounds));

    [self setNeedsLayout];
    [self layoutIfNeeded];

    CGSize calculatedSize = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

    return calculatedSize.height;

}

@end

使用示例:

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath
{
    MYSweetCell *cell = [MYSweetCell heightCalculationCellFromNibWithName:NSStringFromClass([MYSweetCell class])];
    CGFloat height = [cell heightAfterAutoLayoutPassAndRenderingWithBlock:^{
        [(id<MYSweetCellRenderProtocol>)cell renderWithModel:someModel];
    }];
    return CGSizeMake(CGRectGetWidth(self.collectionView.bounds), height);
}

值得庆幸的是,我们在iOS8中不再需要做这个杂耍,但现在还是有的!


2
你应该可以简单地使用[YourCell new]作为虚拟对象。只要约束代码构建代码在你的实例中被触发,并且你以编程方式触发布局传递,你就可以开始了。 - Adam Waite
1
谢谢!这个有效。你的类别很棒。正是让我意识到这种技术也适用于 UICollectionViews - Ricardo Sanchez-Saez
2
你会如何使用在Storyboard中定义的原型单元来完成这个任务? - David Potter

60

这是我的解决方案:

在加载视图之前,您需要告诉TableViewestimatedHeight。否则它将无法像预期的那样运行。

Objective-C

- (void)viewWillAppear:(BOOL)animated {
    _messageField.delegate = self;
    _tableView.estimatedRowHeight = 65.0;
    _tableView.rowHeight = UITableViewAutomaticDimension;
}

升级到 Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0
}

2
将自动布局正确设置,再加上在viewDidLoad中添加此代码就可以解决问题了。 - Bruce
1
但如果estimatedRowHeight逐行变化怎么办?我是过度估计还是低估计?在tableView中使用的高度应该使用最小值还是最大值? - János
1
@János 这是rowHeight的关键。为了使其按预期行事,您需要使用无边距的约束并将对象与TableViewCell对齐。我假设您正在使用UITextView,因此仍需删除autoscroll=false,否则它将保持高度,并且Relative height将无法按预期运作。 - Eddwin Paz
1
这绝对是最健壮的解决方案。不要担心“estimatedRowHeight”,它主要影响滚动条的大小,而不是实际单元格的高度。在选择高度时要勇敢:它将影响插入/删除动画。 - SwiftArchitect
为什么要去掉边距?难道边距最终不会对视图的锚点产生影响吗? - mfaani
显示剩余4条评论

47

@smileyborg提出的解决方案几乎完美。如果您有自定义单元格,并且想要一个或多个具有动态高度的UILabel,则启用AutoLayout的systemLayoutSizeFittingSize方法将返回CGSizeZero,除非您将所有单元格约束从单元格移动到其contentView中(正如@TomSwift在此处建议的那样:How to resize superview to fit all subviews with autolayout?)。

为此,您需要在自定义UITableViewCell实现中插入以下代码(感谢@Adrian)。

- (void)awakeFromNib{
    [super awakeFromNib];
    for (NSLayoutConstraint *cellConstraint in self.constraints) {
        [self removeConstraint:cellConstraint];
        id firstItem = cellConstraint.firstItem == self ? self.contentView : cellConstraint.firstItem;
        id seccondItem = cellConstraint.secondItem == self ? self.contentView : cellConstraint.secondItem;
        NSLayoutConstraint *contentViewConstraint =
        [NSLayoutConstraint constraintWithItem:firstItem
                                 attribute:cellConstraint.firstAttribute
                                 relatedBy:cellConstraint.relation
                                    toItem:seccondItem
                                 attribute:cellConstraint.secondAttribute
                                multiplier:cellConstraint.multiplier
                                  constant:cellConstraint.constant];
        [self.contentView addConstraint:contentViewConstraint];
    }
}

将@smileyborg的答案与这个混合起来应该可以解决问题。


1
systemLayoutSizeFittingSize 需要在 contentView 上调用,而不是 cell。 - onmyway133

26

我遇到了一个很重要的问题,需要作为答案发布。

@smileyborg的答案大部分是正确的。然而,如果你在自定义单元格类的layoutSubviews方法中有任何代码,比如设置preferredMaxLayoutWidth,那么这段代码将不会运行:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];

有一段时间让我感到困惑。后来我意识到,这是因为它们只在contentView上触发了layoutSubviews,而没有在单元格本身上触发。

我的工作代码看起来像这样:

TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"];
[cell configureWithThirdPartyObject:self.app];
[cell layoutIfNeeded];
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;

请注意,如果您正在创建新的单元格,我相信您不需要调用setNeedsLayout,因为它应该已经设置好了。在保存对单元格的引用时,您应该调用它。无论哪种方式,都不应该有什么问题。

如果您正在使用单元格子类,并设置了像preferredMaxLayoutWidth这样的内容,请注意以下提示。正如@smileyborg所提到的,“您的表视图单元格尚未具有固定到表视图宽度的宽度”。如果您在子类中进行工作而不在视图控制器中进行,则会遇到麻烦。但是您可以使用表格宽度简单地设置单元格框架:

例如,在计算高度时:

self.summaryCell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailDefaultSummaryCell"];
CGRect oldFrame = self.summaryCell.frame;
self.summaryCell.frame = CGRectMake(oldFrame.origin.x, oldFrame.origin.y, self.tableView.frame.size.width, oldFrame.size.height);

(我碰巧缓存了这个特定的单元格以便重用,但这与问题无关。)


20
只要您的单元格布局良好。
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}

更新:您应该使用iOS 8中引入的动态调整大小。


这在我的iOS7上可以工作,现在调用tableView:heightForRowAtIndexPath:中的tableView:cellForRowAtIndexPath:是否可以了? - Ants
好的,所以这个不起作用,但是当我在tableView:cellForRowAtIndexPath:中调用systemLayoutSizeFittingSize:并缓存结果,然后在tableView:heightForRowAtIndexPath:中使用它,只要约束正确设置就可以正常工作! - Ants
只有在使用“dequeueReusableCellWithIdentifier:”而不是“dequeueReusableCellWithIdentifier:forIndexPath:”时,此方法才有效。 - Antoine
2
我真的不认为直接调用tableView:cellForRowAtIndexPath:是一个好方法。 - Itachi

20

(适用于 Xcode 8.x / Xcode 9.x 请参考底部内容)

注意下面在 Xcode 7.x 中可能会导致混淆的问题:

Interface Builder 无法正确处理自动调整大小的单元格设置。即使您的约束条件是绝对有效的,IB仍然会报错并给出令人困惑的建议和错误提示。原因是IB不愿意根据您的约束条件改变行高(以使单元格适应您的内容),而是保持行高不变,并开始建议您更改约束条件,这些您应该忽略。

例如,假设您已经设置好了一切,没有警告,没有错误,一切正常工作。

插入图像描述

现在如果您更改字体大小(在此示例中,我将说明标签字体大小从17.0更改为18.0)。

插入图像描述

由于字体大小增加,标签现在希望占用3行(之前它占用2行)。

如果Interface Builder按照预期工作,它将调整单元格的高度以适应新标签的高度。但是实际发生的情况是IB显示红色自动布局错误图标,并建议您修改抱紧/压缩优先级。

插入图像描述

您应该忽略这些警告。而是可以手动更改行高(选择Cell > 大小检查器 > 行高)。

(对于Xcode 8.x / Xcode 9.x,请参考以下内容)

在 Xcode 8.x / Xcode 9.x 中,Interface Builder能够正确处理自动调整大小的单元格设置,因此不需要手动更改行高。如果您遇到与自动调整大小有关的问题,请确保使用最新版本的Xcode。

在此输入图片描述

我通过点击上下步进器,逐一更改这个高度值直到红色箭头错误消失!(实际上你会得到黄色警告,这时只需执行“更新框架”就可以了,它应该能正常工作)。

* 请注意,您实际上不必解决Interface Builder中出现的这些红色错误或黄色警告 - 在运行时,一切都将正确工作(即使IB显示错误/警告)。只需确保在运行时控制台日志中没有获取任何AutoLayout错误即可。

事实上,尝试始终在IB中更新行高度非常烦人,有时几乎不可能(因为存在小数值).

为了避免令人讨厌的IB警告/错误,您可以选择涉及的视图并在Size Inspector中为属性Ambiguity选择Verify Position Only.

在此输入图片描述


Xcode 8.x / Xcode 9.x似乎(有时)与Xcode 7.x做事情不同,但仍然不正确。例如,即使将compression resistance priority / hugging priority设置为必需(1000),Interface Builder也可能拉伸或裁剪标签以适应单元格(而不是调整单元格高度以适应标签)。在这种情况下,它甚至可能不显示任何AutoLayout警告或错误。或者有时会完全按照上述Xcode 7.x所做的方式执行。


你好,是否可以为包含动态单元格内容的表视图中的单元格提供动态高度? - Jignesh B

19
为了自动调整单元格/行高,需要按照以下步骤进行操作:
  • 分配和实现tableview的dataSource和delegate
  • UITableViewAutomaticDimension分配给rowHeight和estimatedRowHeight
  • 实现delegate/dataSource方法(如heightForRowAt),并返回一个值UITableViewAutomaticDimension

-

Objective C:

// in ViewController.h
#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>

  @property IBOutlet UITableView * table;

@end

// in ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    self.table.dataSource = self;
    self.table.delegate = self;

    self.table.rowHeight = UITableViewAutomaticDimension;
    self.table.estimatedRowHeight = UITableViewAutomaticDimension;
}

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {

    return UITableViewAutomaticDimension;
}

Swift:

@IBOutlet weak var table: UITableView!

override func viewDidLoad() {
    super.viewDidLoad()

    // Don't forget to set dataSource and delegate for table
    table.dataSource = self
    table.delegate = self

    // Set automatic dimensions for row height
    // Swift 4.2 onwards
    table.rowHeight = UITableView.automaticDimension
    table.estimatedRowHeight = UITableView.automaticDimension


    // Swift 4.1 and below
    table.rowHeight = UITableViewAutomaticDimension
    table.estimatedRowHeight = UITableViewAutomaticDimension

}



// UITableViewAutomaticDimension calculates height of label contents/text
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    // Swift 4.2 onwards
    return UITableView.automaticDimension

    // Swift 4.1 and below
    return UITableViewAutomaticDimension
}

在UITableviewCell中使用标签示例:
  • 设置行数为0(并将换行模式设置为truncate tail)
  • 将所有约束条件(顶部、底部、右侧、左侧)设置为相对于其父视图/单元格容器。
  • 可选:如果您希望标签覆盖的垂直区域最小,即使没有数据,请设置标签的最小高度。

enter image description here

注意:如果您有多个具有动态长度的标签(UIElements),这些标签应根据其内容大小进行调整:为您希望具有更高优先级扩展/压缩的标签调整“内容吸附和压缩阻力优先级”。


1
谢谢似乎是一个清晰简单的解决方案,但我有一个问题。我不使用标签,而是使用文本视图,所以当数据被添加时,我需要行高增加。我的问题是如何将信息传递给heightForRowAt。我可以测量我的变化的文本视图的高度,但现在需要改变行高。请帮忙,感激不尽。 - Jeremy Andrews
我没有实现 tableView: heightForRow 数据源,但它对我起作用。 - Hemang
@Hemang - 自iOS 11+起,它可以在没有tableView: heightForRow的情况下正常工作。(对于iOS 10-,需要tableView: heightForRow - Krunal
@ Hemang-我很了解你,这对你来说并不太难 :) - Krunal
1
@Hemang - 解决方案取决于确切的查询定义。这里我为您的查询提供了一般常见的解决方案。将条件放在 tableView: heightForRow 中... if (indexPath.row == 0) { return 100} else { return UITableView.automaticDimension } - Krunal
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接