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

Posted

技术标签:

【中文标题】在 UITableView 中使用自动布局进行动态单元格布局和可变行高【英文标题】:Using Auto Layout in UITableView for dynamic cell layouts & variable row heights 【发布时间】:2013-09-11 16:48:39 【问题描述】:

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

【问题讨论】:

这里是swift 2.3中的示例代码github.com/dpakthakur/DynamicCellHeight 要了解有关拥有多行 UILabel 的大多数答案,您需要全面了解 contentSizepreferredMaxLayoutWidth 的工作原理。见here。话虽如此,如果您正确设置了约束,那么您就不需要preferredMaxLayoutWidth,实际上会产生意想不到的结果。 2020 年重要提示,适用于您的 UILabel。说它通过约束附加到左侧。 您还必须将其附加到右侧,并带有“大于或等于零”约束。 【参考方案1】:

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

ios 8 Sample Project - 需要 iOS 8 iOS 7 Sample Project - 适用于 iOS 7+

概念描述

无论您为哪个 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: 返回以使表格视图实际呈现在屏幕上。)接下来,必须使用确切的内容配置单元格(例如文本、图像等),如果它要显示在表格视图中,它将保持。

然后,强制单元格立即布局其子视图,然后在UITableViewCellcontentView 上使用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;
    

示例项目

iOS 8 Sample Project - 需要 iOS 8 iOS 7 Sample Project - 适用于 iOS 7+

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

Xamarin (C#/.NET)

如果您使用 Xamarin,请查看由 @KentBoogaart 整理的 sample project。

【讨论】:

虽然这运行良好,但我发现它略微低估了所需的尺寸(可能是由于各种舍入问题),我必须在最终高度上添加几个点才能让我的所有文本适合标签内 @Alex311 非常有趣,感谢您提供此示例。我做了一些测试,并在这里写了一些 cmets:github.com/Alex311/TableCellWithAutoLayout/commit/… 我强烈建议为每个单元重用标识符类型缓存单元。每次为高度出列时,您都会从队列中取出一个未添加回来的单元格。缓存可以显着减少调用表格单元格的初始化程序的次数。出于某种原因,当我不缓存时,使用的内存量会随着我滚动而不断增长。 故事板,实际上。我认为这不适用于原型单元(至少无需重新实例化整个 VC)。可以通过在 heightForRowAtIndexPath 中出列、保留单元格并在下次调用 cellForRowAtIndexPath 时返回它来完全避免泄漏。 对于iOS9iOS10 是否仍建议使用iOS8 实现,或者自此答案发布以来是否有新方法?【参考方案2】:

对于 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)

完整的示例代码是here。

【讨论】:

我认为我们在OS8案例中不需要实现heightForRowAtIndexPath函数 正如@eddwinpaz 在另一个答案中所指出的那样,用于锁定行高的约束不包括边距是很重要的。 +1 表示“如果多行标签,请不要忘记将 numberOfLines 设置为 0”,这导致我的单元格大小不是动态的。 称我为控制狂,但即使在 iOS 11 时代,我仍然不喜欢使用 UITableViewAutomaticDimension。过去有一些不好的经历。因此,我通常使用此处列出的 iOS7 解决方案。威廉的注意不要忘记 label.preferredMaxLayoutWidth 在这里救了我。 当我们滚动时,标签开始出现在多行中,并且行的高度也没有增加【参考方案3】:

可变高度 UITableViewCell 的 Swift 示例

为 Swift 3 更新

William Hu 的 Swift 答案很好,但它有助于我在第一次学习做某事时有一些简单而详细的步骤。下面的示例是我在学习制作具有可变单元高度的UITableView 时的测试项目。我基于this basic UITableView example for Swift。

完成的项目应如下所示:

创建一个新项目

它可以只是一个单一视图应用程序。

添加代码

向您的项目添加一个新的 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).")
    

重要提示:

以下两行代码(以及自动布局)使可变单元格高度成为可能:

tableView.estimatedRowHeight = 44.0
tableView.rowHeight = UITableViewAutomaticDimension

设置故事板

向您的视图控制器添加一个表格视图并使用自动布局将其固定到四个侧面。然后将 Table View Cell 拖到 Table View 上。在原型单元格上,拖动一个标签。使用自动布局将标签固定到 Table View Cell 的内容视图的四个边缘。

重要提示:

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

其他 IB 设置

自定义类名和标识符

选择 Table View Cell 并将自定义类设置为MyCustomCell(我们添加的 Swift 文件中的类的名称)。还将标识符设置为cell(与我们在上面的代码中用于cellReuseIdentifier 的字符串相同。

标签的零行

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

连接网点

控制从情节提要中的表格视图拖动到ViewController 代码中的tableView 变量。 对 Prototype 单元格中的 Label 执行相同的操作,将其添加到 MyCustomCell 类中的 myCellLabel 变量。

完成

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

注意事项

此示例仅适用于 iOS 8 及更高版本。如果您仍需要支持 iOS 7,那么这将不适合您。 在您未来的项目中,您自己的自定义单元格可能会有多个标签。确保您将所有内容都固定好,以便自动布局可以确定要使用的正确高度。您可能还必须使用垂直压缩阻力和拥抱。有关详细信息,请参阅 this article。

如果您没有固定前缘和后缘(左右),您可能还需要设置标签的preferredMaxLayoutWidth,以便它知道何时换行。例如,如果您在上面的项目中为标签添加了 Center Horizo​​ntally 约束,而不是固定前缘和后缘,那么您需要将此行添加到 tableView:cellForRowAtIndexPath 方法中:

 cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

另见

Understanding Self Sizing Cells and Dynamic Type in iOS 8 Table View Cells with Varying Row Heights UITableView example for Swift

【讨论】:

preferredMaxLayoutWidth 与单元格的contentSize 相对设置不是更好吗?这样,如果您有一个附件视图或者它被滑动以进行编辑,那么它仍然会被考虑吗? @Honey,你很可能是对的。我已经快一年没跟上 iOS 了,我太生疏了,现在不能很好地回答你。【参考方案4】:

我将 @smileyborg 的 iOS7 解决方案包装在一个类别中

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

该类别还纠正了@wildmonkey 的答案中概述的问题(从笔尖加载单元格和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 ***.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 中做这种爵士乐,但现在就在那里!

【讨论】:

您应该能够简单地使用:[YourCell new] 并将其用作虚拟对象。只要在您的实例中触发了约束代码构建代码,并且您以编程方式触发布局传递,您就可以开始了。 谢谢!这行得通。你的类别很棒。这让我意识到这种技术也适用于UICollectionViews 你将如何使用在故事板中定义的原型单元来做到这一点?【参考方案5】:

这是我的解决方案:

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

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

【讨论】:

正确设置自动布局,并在 viewDidLoad 中添加此代码就可以了。 但是如果estimatedRowHeight 逐行变化呢?我应该高估还是低估?使用我在tableView中使用的最小或最大高度? @János 这是 rowHeight 的重点。要做到这一点,您需要使用没有边距的约束并与对象的 TableViewCell 对齐。我假设您使用的是 UITextView 所以您仍然需要删除 autoscroll=false 否则它将保持高度并且相对高度不会按预期运行。 这是迄今为止最强大的解决方案。不用担心estimatedRowHeight,它主要影响滚动条的大小,而不是实际单元格的高度。选择的高度要加粗:它会影响插入/删除动画。 这里是swift 2.3中的示例代码github.com/dpakthakur/DynamicCellHeight【参考方案6】:

@smileyborg 提出的解决方案几乎是完美的。如果您有一个自定义单元格并且您想要一个或多个具有动态高度的UILabel,那么结合启用自动布局的 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 答案与此混合应该可行。

【讨论】:

systemLayoutSizeFittingSize 需要在 contentView 上调用,而不是在单元格上【参考方案7】:

我刚刚遇到了一个足够重要的问题,可以作为答案发布。

@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);

(我碰巧缓存了这个特定的单元格以供重复使用,但这无关紧要)。

【讨论】:

【参考方案8】:

(对于 Xcode 8.x / Xcode 9.x 在底部读取)

请注意 Xcode 7.x 中的以下问题,这可能会造成混淆:

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

例如,假设您已设置一切正常,没有警告,没有错误,一切正常。

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

由于字体大小增加,标签现在要占 3 行(之前是占 2 行)。

如果界面生成器按预期工作,它将调整单元格的高度以适应新的标签高度。然而实际发生的是 IB 显示红色的自动布局错误图标并建议您修改拥抱/压缩优先级。

您应该忽略这些警告。您可以*做的是手动更改行高(选择单元格 > 大小检查器 > 行高)。

我一次单击更改此高度(使用向上/向下步进器),直到红色箭头错误消失! (您实际上会收到黄色警告,此时只需继续执行“更新帧”,它应该都能正常工作)。

* 请注意,您实际上不必在 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 设置为 required (1000),Interface Builder 也可能会拉伸或剪切标签以适应单元格(而不是调整单元格高度以适应标签周围)。在这种情况下,它甚至可能不会显示任何 AutoLayout 警告或错误。或者有时它完全像 Xcode 7.x 所做的那样,如上所述。

【讨论】:

嗨,是否可以为单元格提供动态高度,即具有带有动态单元格内容的单元格的表格视图。?【参考方案9】:

以防人们仍然对此有问题。我写了一篇关于将 Autolayout 与 UITableViews Leveraging Autolayout For Dynamic Cell Heights 以及一个开源组件一起使用的快速博客文章,以帮助使其更加抽象和易于实现。 https://github.com/Raizlabs/RZCellSizeManager

【讨论】:

链接失效【参考方案10】:

只要你的单元格中的布局是好的。

-(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: 了吗? 好的,所以这不起作用,但是当我在tableView:cellForRowAtIndexPath: 中调用systemLayoutSizeFittingSize: 并缓存结果然后在tableView:heightForRowAtIndexPath: 中使用它时,只要正确设置约束,它就可以正常工作当然! 这只有在你使用 dequeueReusableCellWithIdentifier: 而不是 dequeueReusableCellWithIdentifier:forIndexPath: 时才有效 我真的不认为直接调用 tableView:cellForRowAtIndexPath: 是个好方法。【参考方案11】:

要设置行高和估计行高的自动尺寸,请确保执行以下步骤,自动尺寸对单元格/行高布局有效。

分配和实现 tableview 数据源和委托 将UITableViewAutomaticDimension 分配给rowHeight 和estimatedRowHeight 实现委托/数据源方法(即heightForRowAt并返回一个值UITableViewAutomaticDimension给它)

-

目标 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;

斯威夫特:

@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(&换行模式=截尾) 设置与其父视图/单元格容器相关的所有约束(上、下、左右)。 可选:设置标签的最小高度,如果你想要标签覆盖的最小垂直区域,即使没有数据。

注意:如果您有多个标签(UIElements)具有动态长度,应根据其内容大小进行调整:为您的标签调整'Content Hugging and Compression Resistance Priority'想要以更高的优先级展开/压缩。

【讨论】:

谢谢,这似乎是一个清晰而简单的解决方案,但我有一个问题,我没有使用标签而是使用文本视图,所以我需要在添加数据时增加行高。然后我的问题是将信息传递给 heightForRowAt。我可以测量我更改的文本视图的高度,但现在需要更改行高。希望能得到一些帮助 @JeremyAndrews 当然会帮助你。使用您尝试过的源代码和问题的详细信息提出您的问题。 它对我有用,无需实现 tableView: heightForRow 数据源。 @Hemang - 从 iOS 11+ 开始,它可以在没有 tableView: heightForRow 的情况下工作。 (对于 iOS 10-tableView: heightForRow 是必需的) @Hemang - 解决方案取决于确切的查询定义。在这里,我为您的查询提供了通用的通用解决方案。将条件放入tableView: heightForRow..if (indexPath.row == 0) return 100 else return UITableView.automaticDimension 【参考方案12】:

就像@Bob-Spryn 一样,我遇到了一个足够重要的问题,因此我将其发布为答案。

我为@smileyborg's 的回答苦苦挣扎了一段时间。我遇到的问题是,当您使用 [[YourTableViewCellClass alloc] init] 实例化单元格时,如果您在 IB 中使用附加元素(UILabelsUIButtons 等)在 IB 中定义了原型单元格,则它不会实例化所有该单元格中的其他元素,除非您已编写代码来执行此操作。 (我对initWithStyle也有类似的经历。)

要让情节提要实例化所有其他元素,请使用[tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"] 获取您的单元格(不是[tableView dequeueReusableCellWithIdentifier:forIndexPath:],因为这会导致有趣的问题。)当您这样做时,您在 IB 中定义的所有元素都将被实例化。

【讨论】:

【参考方案13】:

Dynamic Table View Cell Height and Auto Layout

解决故事板自动布局问题的好方法:

- (CGFloat)heightForImageCellAtIndexPath:(NSIndexPath *)indexPath 
  static RWImageCell *sizingCell = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^
    sizingCell = [self.tableView dequeueReusableCellWithIdentifier:RWImageCellIdentifier];
  );

  [sizingCell setNeedsLayout];
  [sizingCell layoutIfNeeded];

  CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
  return size.height;

【讨论】:

这已经在这个问题的公认答案中得到了广泛的介绍。 是的,我知道...但我不想使用 PureLayout,而 dispatch_once 的“技巧”对我帮助很大,仅使用 Storyboard 就可以解决。【参考方案14】:
tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension

【讨论】:

【参考方案15】:

另一个“解决方案”:跳过所有这些挫折并使用 UIScrollView 来获得与 UITableView 外观和感觉相同的结果。

这对我来说是一个痛苦的“解决方案”,我花了 20 多个非常令人沮丧的小时来尝试构建类似 Smileyborg 建议的东西,但在数月和三个 App Store 版本中都失败了。

我的看法是,如果您真的需要 iOS 7 支持(对我们来说,这是必不可少的),那么这项技术就太脆弱了,您会费尽心思去尝试。除非您使用某些高级行编辑功能和/或确实需要支持 1000 多个“行”(在我们的应用中,实际上不会超过 20 行),否则 UITableView 通常完全是多余的。

额外的好处是代码变得非常简单,而不是 UITableView 附带的所有委托废话和来回。这只是 viewOnLoad 中的一个代码循环,看起来优雅且易于管理。

以下是一些有关如何操作的提示:

    使用 Storyboard 或 nib 文件,创建 ViewController 和关联的根视图。

    将 UIScrollView 拖到您的根视图上。

    向顶层视图添加约束顶部、底部、左侧和右侧约束,以便 UIScrollView 填充整个根视图。

    在 UIScrollView 中添加一个 UIView 并将其称为“容器”。向 UIScrollView(其父级)添加顶部、底部、左侧和右侧约束。关键技巧:还添加一个“等宽”约束来链接 UIScrollView 和 UIView。

    注意:您将收到错误消息“滚动视图具有不明确的可滚动内容高度”,并且您的容器 UIView 的高度应为 0 像素。应用程序运行时,这两个错误似乎都无关紧要。

    为每个“单元”创建 nib 文件和控制器。使用 UIView 而不是 UITableViewCell。

    在您的根 ViewController 中,您基本上将所有“行”添加到容器 UIView 并以编程方式添加约束,将它们的左右边缘链接到容器视图,它们的顶部边缘到容器视图顶部(对于第一项)或上一个单元格。然后将最后一个单元格链接到容器底部。

对我们来说,每个“行”都在一个 nib 文件中。所以代码看起来像这样:

class YourRootViewController 

    @IBOutlet var container: UIView! //container mentioned in step 4

    override func viewDidLoad() 
        
        super.viewDidLoad()

        var lastView: UIView?
        for data in yourDataSource 

            var cell = YourCellController(nibName: "YourCellNibName", bundle: nil)
            UITools.addViewToTop(container, child: cell.view, sibling: lastView)
            lastView = cell.view
            //Insert code here to populate your cell
        

        if(lastView != nil) 
            container.addConstraint(NSLayoutConstraint(
                item: lastView!,
                attribute: NSLayoutAttribute.Bottom,
                relatedBy: NSLayoutRelation.Equal,
                toItem: container,
                attribute: NSLayoutAttribute.Bottom,
                multiplier: 1,
                constant: 0))
        

        ///Add a refresh control, if you want - it seems to work fine in our app:
        var refreshControl = UIRefreshControl()
        container.addSubview(refreshControl!)
    

这是 UITools.addViewToTop 的代码:

class UITools 
    ///Add child to container, full width of the container and directly under sibling (or container if sibling nil):
    class func addViewToTop(container: UIView, child: UIView, sibling: UIView? = nil)
    
        child.setTranslatesAutoresizingMaskIntoConstraints(false)
        container.addSubview(child)
        
        //Set left and right constraints so fills full horz width:
        
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Leading,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Left,
            multiplier: 1,
            constant: 0))
        
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Trailing,
            relatedBy: NSLayoutRelation.Equal,
            toItem: container,
            attribute: NSLayoutAttribute.Right,
            multiplier: 1,
            constant: 0))
        
        //Set vertical position from last item (or for first, from the superview):
        container.addConstraint(NSLayoutConstraint(
            item: child,
            attribute: NSLayoutAttribute.Top,
            relatedBy: NSLayoutRelation.Equal,
            toItem: sibling == nil ? container : sibling,
            attribute: sibling == nil ? NSLayoutAttribute.Top : NSLayoutAttribute.Bottom,
            multiplier: 1,
            constant: 0))
    

到目前为止,我发现使用这种方法的唯一“问题”是 UITableView 在滚动时在视图顶部具有“浮动”部分标题的不错功能。除非您添加更多编程,否则上述解决方案不会这样做,但对于我们的特殊情况,此功能并不是 100% 必不可少的,并且当它消失时没有人注意到。

如果您想要单元格之间的分隔线,只需在您的自定义“单元格”底部添加一个 1 像素高的 UIView,看起来像分隔线。

请务必打开“反弹”和“垂直反弹”以使刷新控件正常工作,因此它看起来更像一个表格视图。

TableView 在您的内容下显示一些空行和分隔符,如果它没有填满整个屏幕,而这个解决方案没有。但就个人而言,我更喜欢那些空行无论如何都不存在 - 由于单元格高度可变,无论如何在我看来总是有空行。

希望其他程序员在浪费 20 多个小时试图在他们自己的应用程序中使用 Table View 解决问题之前阅读我的帖子。 :)

【讨论】:

感谢您的回答。我会试试的!【参考方案16】:

我必须使用动态视图(通过代码设置视图和约束),当我想设置 preferredMaxLayoutWidth 标签的宽度为 0。所以我得到了错误的单元格高度。

然后我添加了

[cell layoutSubviews];

执行前

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];

在该标签的宽度符合预期并且动态高度计算正确之后。

【讨论】:

【参考方案17】:

假设您有一个带有子视图的单元格,并且您希望该单元格的高度足够高以包含子视图 + 填充。

1) 设置子视图的底部约束等于 cell.contentView 减去你想要的填充。不要对单元格或 cell.contentView 本身设置约束。

2) 将 tableView 的rowHeight 属性或tableView:heightForRowAtIndexPath: 设置为UITableViewAutomaticDimension

3) 将 tableView 的 estimatedRowHeight 属性或 tableView:estimatedHeightForRowAtIndexPath: 设置为对高度的最佳猜测。

就是这样。

【讨论】:

【参考方案18】:

如果您以编程方式进行布局,以下是在 Swift 中使用锚点的 iOS 10 需要考虑的事项。

共有三个规则/步骤

NUMBER 1:在 viewDidLoad 上设置 tableview 的这两个属性,第一个告诉 tableview 应该期望其单元格上的动态大小,第二个只是让应用程序计算滚动条指示器的大小,所以它有助于提高性能。

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

NUMBER 2:这很重要,您需要将子视图添加到单元格的 contentView 而不是视图中,并且还使用它的 layoutsmarginguide 将子视图锚定到顶部和底部,这是一个如何做的工作示例它。

override init(style: UITableViewCellStyle, reuseIdentifier: String?) 
    super.init(style: style, reuseIdentifier: reuseIdentifier)
    setUpViews()


private func setUpViews() 

    contentView.addSubview(movieImageView)
    contentView.addSubview(descriptionLabel)
    let marginGuide = contentView.layoutMarginsGuide

    NSLayoutConstraint.activate([
        movieImageView.heightAnchor.constraint(equalToConstant: 80),
        movieImageView.widthAnchor.constraint(equalToConstant: 80),
        movieImageView.leftAnchor.constraint(equalTo: marginGuide.leftAnchor),
        movieImageView.topAnchor.constraint(equalTo: marginGuide.topAnchor, constant: 20),

        descriptionLabel.leftAnchor.constraint(equalTo: movieImageView.rightAnchor, constant: 15),
        descriptionLabel.rightAnchor.constraint(equalTo: marginGuide.rightAnchor),
        descriptionLabel.bottomAnchor.constraint(equalTo: marginGuide.bottomAnchor, constant: -15),
        descriptionLabel.topAnchor.constraint(equalTo: movieImageView.topAnchor)

        ])

创建一个将添加子视图并执行布局的方法,在 init 方法中调用它。

数字 3:不要调用该方法:

  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat 
    

如果你这样做,你将覆盖你的实现。

对于表格视图中的动态单元格,请遵循这 3 条规则。

这是一个有效的实现 https://github.com/jamesrochabrun/MinimalViewController

【讨论】:

在 Swift 5 中 UITableViewAutomaticDimension 重命名为 UITableView.automaticDimension【参考方案19】:

如果你有一个 long 字符串。例如没有有换行符的。那么你可能会遇到一些问题。

接受的答案和其他几个答案都提到了“所谓的”修复。你只需要添加

cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

我发现Suragh's answer 最完整和简洁,因此不会混淆。

虽然没有解释为什么需要这些更改。让我们这样做。

将以下代码放入项目中。

import UIKit

class ViewController: UIViewController 

    lazy var label : UILabel = 
        let lbl = UILabel()
        lbl.translatesAutoresizingMaskIntoConstraints = false
        lbl.backgroundColor = .red
        lbl.textColor = .black
        return lbl
    ()

    override func viewDidLoad() 
        super.viewDidLoad()
        // step0: (0.0, 0.0)
        print("empty Text intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step1: (29.0, 20.5)
        label.text = "hiiiii"
        print("hiiiii intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step2: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints"
        print("1 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step3: (992.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints translatesAutoresizingMaskIntoConstraints"
        print("3 translate intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step4: (328.0, 20.5)
        label.text = "translatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints\ntranslatesAutoresizingMaskIntoConstraints"
        print("3 translate w/ line breaks (but the line breaks get ignored, because numberOfLines is defaulted to `1` and it will force it all to fit into one line! intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step5: (328.0, 61.0)
        label.numberOfLines = 0
        print("3 translate w/ line breaks and '0' numberOfLines intrinsicContentSize: \(label.intrinsicContentSize)")
        // ----------
        // step6: (98.5, 243.5)
        label.preferredMaxLayoutWidth = 100
        print("3 translate w/ line breaks | '0' numberOfLines | preferredMaxLayoutWidth: 100 intrinsicContentSize: \(label.intrinsicContentSize)")

        setupLayout()
    
    func setupLayout()
        view.addSubview(label)
        label.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
        label.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
    

请注意,我没有添加任何 size 约束。我只添加了 centerX、centerY 约束。但是标签的大小仍然会正确,为什么?

因为contentSize

为了更好地处理这个,首先保留第0步,然后注释掉第1-6步。让setupLayout()留下。观察行为。

然后取消注释step1,并观察。

然后取消注释 step2 并观察。

执行此操作,直到您取消所有 6 个步骤的注释并观察它们的行为。

从这一切可以得出什么结论?哪些因素可以改变contenSize

    文本长度:如果你有更长的文本,那么你的intrinsicContentSize的宽度会增加 换行符:如果添加\n,则intrinsicContentSize 的宽度将是所有行的最大宽度。如果一行有 25 个字符,另一行有 2 个字符,另一行有 21 个字符,那么您的宽度将根据 25 个字符计算 允许的行数:您必须将numberOfLines 设置为0,否则您将不会有多行。您的 numberOfLines 将调整您的 intrinsicContentSize 的 height

    进行调整:想象一下,根据您的文本,您的 intrinsicContentSize 的宽度为 200,高度为 100,但您想将宽度限制为标签的容器你打算做什么?解决方案是将其设置为所需的宽度。您可以通过将preferredMaxLayoutWidth 设置为130 来做到这一点,那么您的新intrinsicContentSize 将具有大约130 的宽度。高度显然会超过100,因为你需要更多的行。 话虽如此,如果您的约束设置正确,那么您根本不需要使用它!有关更多信息,请参阅this answer 及其 cmets。如果你没有限制宽度/高度的约束,你只需要使用preferredMaxLayoutWidth,因为有人可能会说“不要换行,除非它超过preferredMaxLayoutWidth”。但是,如果您将前导/尾随和 numberOfLines 设置为 0,那么您就可以 100% 确定! 长话短说,这里推荐使用它的大多数答案都是错误的!你不需要它。需要它表明您的约束设置不正确或您只是没有约束

    字体大小: 另请注意,如果您增加 fontSize,则 intrinsicContentSize 的 height 会增加。我没有在我的代码中显示这一点。你可以自己试试。

回到你的 tableViewCell 示例:

您需要做的就是:

numberOfLines 设置为0 将标签正确地限制在边距/边缘 无需设置preferredMaxLayoutWidth

【讨论】:

【参考方案20】:

在我的情况下,我必须使用来自服务器的图像创建一个自定义单元格,并且可以是任何宽度和高度。以及两个具有动态大小(宽度和高度)的 UILabel

在我的回答中,我通过自动布局和编程实现了同样的效果:

基本上上面@smileyBorg 的回答有所帮助,但 systemLayoutSizeFittingSize 对我没有用,在我的方法中:

1.不使用自动行高计算属性。 2.不使用估计的高度 3.不需要不必要的updateConstraints。 4.不使用自动首选最大布局宽度。 5. 不使用 systemLayoutSizeFittingSize (应该可以使用但不适合我,我不知道它在内部做什么),而是我的方法 -(float)getViewHeight 工作,我知道它在内部做什么.

Is it possible to have differing heights in a UITableView Cell when I use several different ways of displaying the cell?

【讨论】:

【参考方案21】:

在我的情况下,填充是因为 sectionHeader 和 sectionFooter 高度,故事板允许我将其更改为最小 1。所以在 viewDidLoad 方法中:

tableView.sectionHeaderHeight = 0
tableView.sectionFooterHeight = 0

【讨论】:

【参考方案22】:

我只是对 rowHeightestimatedRowHeight 的 2 个值进行了一些愚蠢的尝试和错误,只是认为它可能会提供一些调试见解:

如果您同时设置它们或只设置estimatedRowHeight,您将获得所需的行为:

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1.00001 // MUST be greater than 1

建议您尽最大努力获得正确的估算值,但最终结果并没有什么不同。它只会影响你的表现。


如果你只设置 rowHeight 即只做:

tableView.rowHeight = UITableViewAutomaticDimension

你的最终结果不会如你所愿:


如果您将estimatedRowHeight 设置为1 或更小,那么无论rowHeight 如何,您都会崩溃

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 1 

我因以下错误消息而崩溃:

Terminating app due to uncaught exception
'NSInternalInconsistencyException', reason: 'table view row height
must not be negative - provided height for index path (<NSIndexPath:
0xc000000000000016> length = 2, path = 0 - 0) is -1.000000'
    ...some other lines...

libc++abi.dylib: terminating with uncaught exception of type
NSException

【讨论】:

【参考方案23】:

关于@smileyborg 接受的答案,我发现

[cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize]

在某些约束不明确的情况下不可靠。最好强制布局引擎计算一个方向的高度,方法是使用下面 UIView 上的帮助器类别:

-(CGFloat)systemLayoutHeightForWidth:(CGFloat)w
    [self setNeedsLayout];
    [self layoutIfNeeded];
    CGSize size = [self systemLayoutSizeFittingSize:CGSizeMake(w, 1) withHorizontalFittingPriority:UILayoutPriorityRequired verticalFittingPriority:UILayoutPriorityFittingSizeLevel];
    CGFloat h = size.height;
    return h;

其中 w: 是 tableview 的宽度

【讨论】:

【参考方案24】:

只需在您的视图控制器中添加这两个功能即可解决您的问题。在这里,list 是一个字符串数组,其中包含每一行的字符串。

 func tableView(_ tableView: UITableView, 
   estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat 
        tableView.rowHeight = self.calculateHeight(inString: list[indexPath.row])

    return (tableView.rowHeight) 


func calculateHeight(inString:String) -> CGFloat

    let messageString = input.text
    let attributes : [NSAttributedStringKey : Any] = [NSAttributedStringKey(rawValue: NSAttributedStringKey.font.rawValue) : UIFont.systemFont(ofSize: 15.0)]

    let attributedString : NSAttributedString = NSAttributedString(string: messageString!, attributes: attributes)

    let rect : CGRect = attributedString.boundingRect(with: CGSize(width: 222.0, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil)

    let requredSize:CGRect = rect
    return requredSize.height

【讨论】:

【参考方案25】:
swift 4

    @IBOutlet weak var tableViewHeightConstraint: NSLayoutConstraint!
    @IBOutlet weak var tableView: UITableView!
    private var context = 1
 override func viewDidLoad() 
        super.viewDidLoad()

        self.tableView.addObserver(self, forKeyPath: "contentSize", options: [.new,.prior], context: &context)
    
  // Added observer to adjust tableview height based on the content

    override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) 
        if context == &self.context
            if let size = change?[NSKeyValueChangeKey.newKey] as? CGSize
                print("-----")
                print(size.height)
                tableViewHeightConstraint.constant = size.height + 50
            
        
    

//Remove observer
 deinit 

        NotificationCenter.default.removeObserver(self)

    

【讨论】:

【参考方案26】:

如果单元格的高度是由内容动态的,你应该精确地计算出来,然后在单元格被渲染之前返回高度值。一种简单的方法是在表格视图单元格代码中定义计数方法,以便控制器在表格单元格高度委托方法处调用。如果高度依赖于表格或屏幕的宽度,请不要忘记计算实际单元格框架宽度(默认为 320)。即在表格单元格高度委托方法中,先使用cell.frame修正单元格宽度,然后调用单元格中定义的计数高度方法得到合适的值并返回

PS。生成单元格对象的代码可以定义在另一个方法中,供不同的表格视图单元格委托方法调用。

【讨论】:

【参考方案27】:

UITableView.automaticDimension 可以通过 Interface Builder 设置:

Xcode > 故事板 > 大小检查器

表格视图单元格 > 行高 > 自动

【讨论】:

@pkamb ..为什么这个答案.....没想到你的 SO 24.9k 点 ;) @Wings 不正确吗?这是我在搜索这个问题时需要的答案。我不明白反对票。 @pkamb....有问题的是他在问如何设置单元格的布局;)【参考方案28】:

Swift 中的另一个 iOs7+iOs8 解决方案

var cell2height:CGFloat=44

override func viewDidLoad() 
    super.viewDidLoad()
    theTable.rowHeight = UITableViewAutomaticDimension
    theTable.estimatedRowHeight = 44.0;


func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell 
    let cell =  tableView.dequeueReusableCellWithIdentifier("myTableViewCell", forIndexPath: indexPath) as! myTableViewCell
    cell2height=cell.contentView.height
    return cell


func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat 
    if #available(iOS 8.0, *) 
        return UITableViewAutomaticDimension
     else 
        return cell2height
    

【讨论】:

注意:systemLayoutSizeFittingSize 在我的情况下不起作用 cellForRowAtIndexPath 中的单元格高度不正确,此时单元格尚未布局。 在 iOs7 中它是固定值,即它可以工作。如果需要,您可以在 cellForRowAtIndexPath 外部设置

以上是关于在 UITableView 中使用自动布局进行动态单元格布局和可变行高的主要内容,如果未能解决你的问题,请参考以下文章

具有自动布局的 UITableView 中的动态列

自动布局动态大小 uitableview 单元格的最佳方法是啥

使用自动布局的 UITableView 中的动态 UITextView 高度?

动态 UITableView 标头(不是节标头)自动布局问题

如何使用动态单元格高度和自动布局以编程方式创建一个非常基本的 UITableView?

具有自动布局的 UITableView 中的动态单元格高度