题 在UITableView中使用自动布局来获取动态单元格布局和可变行高


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


1350
2017-09-11 16:48


起源


这是swift 2.3中的示例代码 github.com/dpakthakur/DynamicCellHeight - Deepak Thakur


答案:


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

概念描述

无论您正在开发哪种iOS版本,以下前两个步骤均适用。

1.设置和添加约束

在你的 UITableViewCell 子类,添加约束,以便单元格的子视图的边缘固定到单元格的边缘 内容查看 (最重要的是顶部和底部边缘)。 注意:不要将子视图固定到单元格本身;只对细胞而言 contentView 通过确保这些子视图的内在内容大小来驱动表视图单元格内容视图的高度 内容抗压性 和 内容拥抱 每个子视图的垂直维度中的约束不会被您添加的更高优先级约束覆盖。 (咦?点击这里。

请记住,我们的想法是将单元格的子视图垂直连接到单元格的内容视图,以便它们可以“施加压力”并使内容视图扩展以适应它们。使用带有几个子视图的示例单元格,这里是一个可视化的例子 一些  (不是全部!) 你的约束需要看起来像:

Example illustration of constraints on a table view cell.

您可以想象,随着更多文本被添加到上面示例单元格中的多线体标签,它将需要垂直增长以适合文本,这将有效地迫使细胞在高度上增长。 (当然,您需要正确的约束才能使其正常工作!)

让你的约束正确是绝对的 最难和最重要的部分 使用自动布局获取动态单元格高度。如果你在这里犯了错误,它可以阻止其他一切工作 - 所以慢慢来!我建议在代码中设置约束,因为您确切地知道在哪里添加了哪些约束,并且在出现问题时更容易调试。在代码中添加约束可以像Interface Builder一样简单,也可以使用布局锚点或GitHub上提供的一个非常棒的开源API。

  • 如果您在代码中添加约束,则应该从内部执行此操作 updateConstraints UITableViewCell子类的方法。注意 updateConstraints 可能会多次调用,因此为了避免多次添加相同的约束,请确保将约束添加代码包装在其中 updateConstraints 检查一个布尔属性,如 didSetupConstraints (运行约束添加代码一次后设置为YES)。另一方面,如果您有更新现有约束的代码(例如调整 constant在一些约束上的属性),把它放在 updateConstraints 但在检查之外 didSetupConstraints 所以它可以在每次调用方法时运行。

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

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

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

请注意,由于内在内容大小的差异,具有相同约束(类型)的单元格可能仍然具有不同的高度!由于内容的大小不同,不要将根本不同的布局(不同的约束)与不同的计算视图帧(由相同的约束条件求解)混淆。

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

适用于iOS 8 - Self-Sizing Cells

3.启用行高估计

要启用自行调整表格视图单元格,必须设置表格视图   rowHeight属性为UITableViewAutomaticDimension。你也必须   为estimatedRowHeight属性赋值。两个都   设置这些属性后,系统使用自动布局来计算   行的实际高度

苹果: 使用自定义表视图单元格

在iOS 8中,Apple已经内置了以前必须在iOS 8之前实现的大部分工作。为了使自定义单元机制能够工作,您必须首先设置 rowHeight 表上的属性查看常量 UITableViewAutomaticDimension。然后,您只需通过设置表视图来启用行高估计 estimatedRowHeight 属性为非零值,例如:

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

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

一般来说,您提供的估计值不必非常准确 - 它仅用于在表格视图中正确调整滚动指示器的大小,并且表格视图可以很好地调整滚动指示器以获得不正确的估计值在屏幕上滚动单元格。你应该设置 estimatedRowHeight 表视图上的属性(in viewDidLoad 或类似的)到一个常数值,即“平均”行高。 只有当您的行高具有极端的可变性(例如,相差一个数量级)并且您在滚动时注意到滚动指示符“跳跃”,您才打算执行 tableView:estimatedHeightForRowAtIndexPath: 进行所需的最小计算,以便为每一行返回更准确的估计值。

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

3.做一个布局通行证并获取单元格高度

首先,实例化一个表视图单元的屏幕外实例, 每个重用标识符的一个实例,严格用于高度计算。 (屏幕外意味着单元格引用存储在视图控制器上的属性/ ivar中,并且从不返回 tableView:cellForRowAtIndexPath: 表格视图实际上在屏幕上呈现。)接下来,必须配置单元格,如果要在表格视图中显示,它将具有确切的内容(例如文本,图像等)。

然后,强制单元格立即布局其子视图,然后使用 systemLayoutSizeFittingSize: 关于的方法 UITableViewCellcontentView 找出所需的细胞高度。使用 UILayoutFittingCompressedSize 获得适合细胞所有内容所需的最小尺寸。然后可以从高度返回高度 tableView:heightForRowAtIndexPath: 委托方法。

4.使用估计行高

如果你的表视图中有超过几十行,你会发现在第一次加载表视图时,执行自动布局约束求解会很快陷入主线程,如 tableView:heightForRowAtIndexPath: 在第一次加载时调用每一行(为了计算滚动指示符的大小)。

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

一般来说,您提供的估计值不必非常准确 - 它仅用于在表格视图中正确调整滚动指示器的大小,并且表格视图可以很好地调整滚动指示器以获得不正确的估计值在屏幕上滚动单元格。你应该设置 estimatedRowHeight 表视图上的属性(in viewDidLoad 或类似的)到一个常数值,即“平均”行高。 只有当您的行高具有极端的可变性(例如,相差一个数量级)并且您在滚动时注意到滚动指示符“跳跃”,您才打算执行 tableView:estimatedHeightForRowAtIndexPath: 进行所需的最小计算,以便为每一行返回更准确的估计值。

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

如果你已经完成了以上所有工作,并且仍然发现在进行约束求解时性能会慢得令人无法接受 tableView:heightForRowAtIndexPath:,遗憾的是,你需要为单元格高度实现一些缓存。 (这是Apple工程师建议的方法。)一般的想法是让Auto Layout引擎第一次解决约束,然后缓存该单元格的计算高度,并将缓存值用于该单元格高度的所有未来请求。当然,技巧是确保在发生任何可能导致单元格高度发生变化的情况时清除单元格的缓存高度 - 主要是当单元格的内容发生变化或其他重要事件发生时(如用户调整)动态类型文本大小滑块)。

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

示例项目

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

Xamarin(C#/。NET)

如果您正在使用Xamarin,请查看此信息 示例项目 放在一起 @KentBoogaart


2268
2018-06-19 08:21



如果我在那里95%的路上,剩下的5%将花费我半个星期来弄明白!我非常感谢你这么快就投入并伸出援助之手。工作代码在GitHub上运行,因此社区的其他人可以受益。笑脸真的很棒! - caoimghgin
@ Alex311非常有趣,谢谢你提供这个例子。我在最后做了一些测试,并在这里写了一些评论: github.com/Alex311/TableCellWithAutoLayout/commit/... - smileyborg
我强烈建议为每个单元重用标识符类型缓存单元。每次出列高度时,都会将一个单元格从未添加回的队列中取出。缓存可以显着降低调用表格单元格的初始化程序的次数。出于某种原因,当我没有缓存时,使用的内存量随着滚动而不断增长。 - Alex311
认真的....苹果不能只是简单地允许cell.rowHeight = DYNAMIC_HEIGHT;返回细胞; ?????他们在想什么? - user1504605
@NathanBuggia,哦,天啊!亲爱的Apple,是不是很难放 wrap_content 哪有啊? - guness


对于IOS8来说,它非常简单:

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableViewAutomaticDimension
}

要么

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}

但对于IOS7,关键是计算自动布局后的高度,

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)

完整的示例代码是 这里


122
2018-04-05 09:23



我认为我们不需要在OS8的情况下实现heightForRowAtIndexPath函数 - jpulikkottil
正如@eddwinpaz在另一个答案中指出的那样,用于锁定行高的约束不包括边距是很重要的。 - Richard
+“如果有多行标签,请不要忘记将numberOfLines设置为0”,这导致我的单元格大小不动态。 - Ian-Fogelman
叫我控制狂,但即使在iOS 11的时代,我仍然不喜欢使用UITableViewAutomaticDimension。过去有一些不好的经历。因此,我通常使用此处列出的iOS7解决方案。威廉的注意事项不要忘记label.preferredMaxLayoutWidth这里救了我。 - Brian Sachetta


可变高度UITableViewCell的Swift示例

针对Swift 3进行了更新

William Hu的Swift答案很好,但是当我第一次学习做某事时,它帮助我做了一些简单而详细的步骤。以下示例是我的测试项目,同时学习制作 UITableView 具有可变的单元格高度。我立足于此 这是Swift的基本UITableView示例

完成的项目应如下所示:

enter image description here

创建一个新项目

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

添加代码

将新的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
    

设置故事板

将表视图添加到视图控制器并使用自动布局将其固定到四个边。然后将表视图单元格拖到表视图上。在Prototype单元格上,拖动一个Label。使用自动布局将标签固定到表视图单元格的内容视图的四个边缘。

enter image description here

重要的提示:

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

其他IB设置

自定义类名和标识符

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

enter image description here

标签零线

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

enter image description here 

连接奥特莱斯

  • 控制从故事板中的表视图拖动到 tableView 变量 ViewController 码。
  • 对Prototype单元格中的Label执行相同操作 myCellLabel 变量 MyCustomCell 类。

成品

您应该能够立即运行项目并获得具有可变高度的单元格。

笔记

  • 此示例仅适用于iOS 8及更高版本。如果您仍然需要支持iOS 7,那么这对您不起作用。
  • 您未来项目中的自定义单元格可能只有一个标签。确保将所有内容都固定,以便自动布局可以确定要使用的正确高度。您可能还必须使用垂直压缩阻力和拥抱。看到 本文 了解更多相关信息。
  • 如果您没有固定前端和尾部(左侧和右侧)边缘,您可能还需要设置标签 preferredMaxLayoutWidth 这样它就知道何时换行。例如,如果您在上面的项目中为标签添加了一个中心水平约束而不是固定前缘和后缘,那么您需要将此行添加到 tableView:cellForRowAtIndexPath 方法:

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    

也可以看看


71
2018-06-11 15:18





我将@ 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 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中做这个爵士乐,但现在就是这样!


60
2018-02-10 19:41



您应该只需使用: [YourCell new] 并将其用作假人。只要在您的实例中触发了约束代码构建代码,并且以编程方式触发布局传递,您就应该好了。 - Adam Waite
谢谢!这很有效。你的类别很棒。这让我意识到这种技术可以使用 UICollectionViews 同样。 - Ricardo Sanchez-Saez
您如何使用故事板中定义的原型单元格来完成此操作? - David Potter


这是我的解决方案。在加载视图之前,您需要告诉TableView estimatedHeight。否则它将无法像预期的那样表现。

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

43
2017-11-11 16:52



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


@smileyborg提出的解决方案几乎是完美的。如果您有一个自定义单元格,并且您想要一个或多个 UILabel 然后是动态高度 systemLayoutSizeFittingSize 结合启用AutoLayout的方法返回a CGSizeZero 除非您将所有单元格约束从单元格移动到其contentView(正如@TomSwift在此处所建议的那样) 如何调整superview的大小以适应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答案应该有效。


42
2017-12-03 02:23



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


一个重要的问题我刚刚碰到发布作为答案。

@ smileyborg的回答大多是正确的。但是,如果你有任何代码 layoutSubviews 自定义单元类的方法,例如设置 preferredMaxLayoutWidth,然后它将不会与此代码一起运行:

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

这让我困惑了一段时间。然后我意识到这是因为那些只是在触发layoutSubviews contentView而不是细胞本身。

我的工作代码如下所示:

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

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


22
2018-03-05 15:53





如果人们仍然遇到这个问题。我写了一篇关于在UITableViews中使用Autolayout的快速博客文章 利用Autolayout实现动态细胞高度 以及一个开源组件,以帮助使这更抽象,更容易实现。 https://github.com/Raizlabs/RZCellSizeManager


19
2018-06-27 22:49





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

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

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

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

enter image description here

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

enter image description here

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

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

enter image description here

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

enter image description here

我一次改变这个高度一次(使用向上/向下步进)直到红色箭头错误消失! (你实际上会得到黄色警告,此时只需要继续做'更新帧',它应该全部工作)。

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

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

为了防止恼人的IB警告/错误,您可以选择所涉及的视图 Size Inspector 为了财产 Ambiguity 选择 Verify Position Only

enter image description here


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


18
2018-04-29 18:16



哇谢谢你!!在使用自动调整大小单元的2个操作系统版本之后,我认为苹果不会是这种愚蠢!但事实证明我们需要等待ios15才能正常工作。无论如何,你从黑客攻击细胞和其他视图等拥抱属性中省去了麻烦。 - EralpB
嗨是可以为细胞提供动态高度,具有带有动态细胞内容的细胞的tableview。 - Jignesh B


只要您的单元格中的布局良好。

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

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

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


16
2017-07-08 01:34



在iOS7上这对我有用,现在可以打电话了 tableView:cellForRowAtIndexPath: 在 tableView:heightForRowAtIndexPath: 现在? - Ants
好的,这不起作用,但是当我打电话时 systemLayoutSizeFittingSize: 在 tableView:cellForRowAtIndexPath: 然后缓存结果,然后使用它 tableView:heightForRowAtIndexPath: 只要约束设置正确,它就可以正常工作! - Ants
在iOS 7中它可以工作。如何让它在iOS 8中运行? - Winston
这仅适用于使用dequeueReusableCellWithIdentifier:而不是dequeueReusableCellWithIdentifier:forIndexPath: - Antoine
我真的不认为调用tableView:cellForRowAtIndexPath:直接是一个好方法。 - Itachi