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

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

答案

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

概念描述

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

1. 设置和添加约束

UITableViewCell子类中,添加约束,以使单元格的子视图的边缘固定到单元格contentView的边缘(最重要的是顶部和底部边缘)。 注意:不要将子视图固定到单元格本身;仅对单元格的contentView让这些子视图的固有内容大小驱动表格视图单元格的内容视图的高度,方法是确保每个子视图的垂直方向的内容压缩阻力内容拥抱约束都不会被添加的更高优先级约束所覆盖。 ( 嗯?点击这里。

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

对表视图单元格的约束的示例说明。

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

正确使用约束绝对是使用自动版式获得动态像元高度时最困难也是最重要的部分 。如果您在此处输入错误,则可能会阻止其他所有功能运行 - 因此,请花点时间!我建议您在代码中设置约束,因为您确切知道要在哪里添加约束,并且在出现问题时调试起来要容易得多。与使用布局锚点或 GitHub 上一种出色的开源 API 之一的 Interface Builder 相比,在代码中添加约束既简单又强大,而且功能强大。

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

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

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

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

请注意,由于内部内容大小的不同,具有相同约束(类型)的单元格可能仍具有不同的高度!不要将根本不同的布局(不同的约束)与由于内容大小不同而导致的计算出的不同视图框(由相同约束解决)混淆。

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

对于 iOS 8 - 自定义单元格

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上表视图(物业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 通用示例代码(有很多多汁的注释)

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

样例项目

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

Xamarin(C#/。NET)

如果您使用的是 Xamarin,请查看@KentBoogaart 编写的示例项目

对于上面的 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)

完整的示例代码在这里

可变高度 UITableViewCell 的 Swift 示例

为 Swift 3 更新

威廉 · 胡(William Hu)的 Swift 回答很好,但是当我第一次学习做某事时,它可以帮助我采取一些简单而详细的步骤。下面的示例是我的学习项目,同时学习制作具有可变单元格高度的UITableView 。我基于Swift 的基本 UITableView 示例

完成的项目应如下所示:

在此处输入图片说明

创建一个新项目

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

添加代码

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

设置情节提要

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

在此处输入图片说明

重要的提示:

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

其他 IB 设置

自定义类名称和标识符

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

在此处输入图片说明

标签零线

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

在此处输入图片说明

连接插座

  • 控制从情节tableView中的 “表视图” 拖动到ViewController代码中的tableView变量。
  • 对 Prototype 单元格中的myCellLabelMyCustomCell类中的myCellLabel变量执行相同的myCellLabel

已完成

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

笔记

  • 此示例仅适用于 iOS 8 及更高版本。如果您仍然需要支持 iOS 7,则此功能将对您不起作用。
  • 在未来的项目中,您自己的自定义单元可能会包含多个标签。确保正确固定所有内容,以便自动布局可以确定要使用的正确高度。您可能还必须使用垂直压缩阻力和抱紧力。有关更多信息,请参见本文
  • 如果不固定前边缘和后边缘(左和右),则可能还需要设置标签的preferredMaxLayoutWidth以便它知道何时换行。例如,如果您在上面的项目中向标签添加了 “水平居中” 约束,而不是固定前缘和后缘,则需要将此行添加到tableView:cellForRowAtIndexPath方法中:

    cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width

也可以看看

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

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

该类别还纠正了 @wildmonkey 的答案中概述的问题(从笔尖和systemLayoutSizeFittingSize:加载单元格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 中进行爵士乐演奏,但是现在就可以了!

这是我的解决方案:

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

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

@smileyborg 提出的解决方案几乎是完美的。如果您有一个自定义单元格,并且想要一个或多个具有动态高度的UILabel ,则将systemLayoutSizeFittingSize方法与 AutoLayout 结合使用会返回CGSizeZero除非您将所有单元格约束从该单元格移至其 contentView(如 @TomSwift 此处所示, 如何调整大小)来让所有子视图都具有自动布局功能? )。

为此,您需要在自定义 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 答案与此混用应该可以。

我刚遇到要发布的答案就足够重要。

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

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

如果人们仍然对此感到麻烦。我写了一篇简短的博客文章,内容涉及将自动布局与 UITableViews 结合使用,以利用动态布局高度的自动布局以及一个开源组件来帮助使这一过程更加抽象和易于实现。 https://github.com/Raizlabs/RZCellSizeManager

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

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

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

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

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

当心 Xcode 7.x 中的以下问题,这可能会引起混乱:

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

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

在此处输入图片说明

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

在此处输入图片说明

因为字体大小增加了,所以标签现在要占用 3 行(在占用 2 行之前)。

如果 Interface Builder 能够按预期工作,它将调整单元格的高度大小以适应新的标签高度。但是,实际情况是 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设置为必需(1000),Interface Builder 也会拉伸或剪切标签以适合单元格(而不是调整单元格高度以适合标签周围)。在这种情况下,它甚至可能不会显示任何 AutoLayout 警告或错误。或者有时它确实可以完成 Xcode 7.x 的操作,如上所述。