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

smi*_*org 1477 row-height uitableview ios autolayout nsautolayout

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

smi*_*org 2360

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

概念描述

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

1.设置和添加约束

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

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

表视图单元格上的约束的示例说明.

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

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

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

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

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

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

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

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

适用于iOS 8 - Self-Sizing Cells

3.启用行高估计

要启用自调整大小的表视图单元格,必须将表视图的rowHeight属性设置为UITableViewAutomaticDimension.您还必须为estimatedRowHeight属性分配值.一旦设置了这两个属性,系统就会使用"自动布局"来计算行的实际高度

Apple:使用自定义表格查看单元格

在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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

然后,迫使立即布局其子视图的细胞,然后使用systemLayoutSizeFittingSize:该方法UITableViewCellcontentView找出电池的必要高度是什么.使用UILayoutFittingCompressedSize去适应单元格中的所有内容所需的最小尺寸.然后可以从tableView:heightForRowAtIndexPath:委托方法返回高度.

4.使用估计行高

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

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

一般来说,您提供的估计不必非常准确 - 它仅用于在表格视图中正确调整滚动指示器的大小,并且表格视图可以很好地调整滚动指示器以获得不正确的估计值在屏幕上滚动单元格.您应该将estimatedRowHeight表视图(在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 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

示例项目

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

Xamarin(C#/.NET)

如果您使用Xamarin,看看这个样本项目由放在一起@KentBoogaart.

  • @ Alex311非常有趣,谢谢你提供这个例子.我在最后做了一些测试,并在这里写了一些评论:https://github.com/Alex311/TableCellWithAutoLayout/commit/bde387b27e33605eeac3465475d2f2ff9775f163#commitcomment-4633188 (5认同)
  • 我强烈建议为每个单元重用标识符类型缓存单元.每次出列高度时,都会将一个单元格从未添加回的队列中取出.缓存可以显着降低调用表格单元格的初始化程序的次数.出于某种原因,当我没有缓存时,使用的内存量会随着滚动而不断增长. (3认同)
  • 是否仍然推荐用于iOS9和iOS10的"iOS8"实现,或者自从这个答案发布以来是否有新的方法? (2认同)

Wil*_* Hu 165

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

override func viewDidLoad() {  
    super.viewDidLoad()

    self.tableView.estimatedRowHeight = 80
    self.tableView.rowHeight = UITableViewAutomaticDimension
}
Run Code Online (Sandbox Code Playgroud)

要么

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return UITableViewAutomaticDimension
}
Run Code Online (Sandbox Code Playgroud)

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

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

重要

  • 如果有多行标签,请不要忘记设置numberOfLines0.

  • 别忘了 label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)

完整的示例代码在这里.

编辑 Swift 4.2 UITableViewAutomaticDimension改为 UITableView.automaticDimension

  • 我认为我们不需要在OS8情况下实现heightForRowAtIndexPath函数 (7认同)

Sur*_*gch 91

可变高度UITableViewCell的Swift示例

针对Swift 3进行了更新

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

完成的项目应如下所示:

在此输入图像描述

创建一个新项目

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

添加代码

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

import UIKit
class MyCustomCell: UITableViewCell {
    @IBOutlet weak var myCellLabel: UILabel!
}
Run Code Online (Sandbox Code Playgroud)

我们稍后会连接这个插座.

打开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).")
    }
}
Run Code Online (Sandbox Code Playgroud)

重要的提示:

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

    tableView.estimatedRowHeight = 44.0
    tableView.rowHeight = UITableViewAutomaticDimension
    
    Run Code Online (Sandbox Code Playgroud)

设置故事板

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

在此输入图像描述

重要的提示:

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

其他IB设置

自定义类名称和标识符

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

在此输入图像描述

标签的零线

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

在此输入图像描述

连接奥特莱斯

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

成品

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

笔记

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

     cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
    
    Run Code Online (Sandbox Code Playgroud)

也可以看看


Ada*_*ite 65

我将@ 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

用法示例:

- (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);
}
Run Code Online (Sandbox Code Playgroud)

值得庆幸的是,我们不必在iOS8中做这个爵士乐,但现在就是这样!

  • 您应该只需使用:`[YourCell new]`并将其用作虚拟对象.只要在您的实例中触发约束代码构建代码,并以编程方式触发布局传递,您就应该好了. (2认同)
  • 您如何使用故事板中定义的原型单元格来完成此操作? (2认同)

Edd*_*Paz 57

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

Objective-C的

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

更新到Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0
}
Run Code Online (Sandbox Code Playgroud)

  • 正确设置自动布局,以及在viewDidLoad中添加此代码的做法. (2认同)

wil*_*key 46

@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];
    }
}
Run Code Online (Sandbox Code Playgroud)

混合@smileyborg答案应该有效.


Bob*_*ryn 25

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

@ smileyborg的回答大多是正确的.但是,如果您layoutSubviews的自定义单元类的方法中有任何代码,例如设置preferredMaxLayoutWidth,那么它将不会使用以下代码运行:

[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
Run Code Online (Sandbox Code Playgroud)

这让我困惑了一段时间.然后我意识到这是因为那些只是触发layoutSubviews contentView,而不是单元本身.

我的工作代码如下所示:

TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"];
[cell configureWithThirdPartyObject:self.app];
[cell layoutIfNeeded];
CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height;
Run Code Online (Sandbox Code Playgroud)

请注意,如果您要创建一个新单元格,我很确定您不需要调用,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);
Run Code Online (Sandbox Code Playgroud)

(我碰巧缓存了这个特殊的细胞以供重复使用,但那是无关紧要的).


Ale*_*use 22

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


Chr*_*irk 19

只要您的单元格布局良好.

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

    return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我真的不认为直接调用 tableView:cellForRowAtIndexPath: 是一个好方法。 (2认同)

Nik*_*iev 19

(对于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设置为required(1000),Interface Builder也可以拉伸或剪裁标签以适合单元格(而不是调整单元格高度以适应标签周围).在这种情况下,它甚至可能不会显示任何AutoLayout警告或错误.或者有时它完全与Xcode 7.x所做的完全相同,如上所述.


Kru*_*nal 18

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

  • 分配并实现tableview dataSource和delegate
  • 分配UITableViewAutomaticDimension给rowHeight和estimatedRowHeight
  • 实现delegate/dataSource方法(即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;
}
Run Code Online (Sandbox Code Playgroud)

迅速:

@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
}
Run Code Online (Sandbox Code Playgroud)

对于UITableviewCell中的标签实例

  • 设置行数= 0(&换行模式=截断尾部)
  • 设置相对于其superview/cell容器的所有约束(顶部,底部,右侧).
  • 可选:如果您希望标签覆盖最小垂直区域,即使没有数据,也可以设置标签的最小高度.

在此输入图像描述

注意:如果您有多个具有动态长度的标签(UIElements),应根据其内容大小进行调整:针对要以更高优先级展开/压缩的标签调整"内容拥抱和压缩阻力优先级".


小智 16

就像@ Bob-Spryn一样,我遇到了一个非常重要的问题,我将其作为答案发布.

我和@ smileyborg的答案挣扎了一段时间.我遇到的问题是,如果你在IB中用IB中的其他元素(UILabels,UIButtons等等)定义了你的原型单元格,那么当你用[ [YourTableViewCellClass alloc] init]不会实例化那个单元格中的所有其他元素时,除非你已经实例化了编写代码来做到这一点.(我有类似的经历initWithStyle.)

让故事板实例化所有其他元素获取你的单元格[tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"](不是[tableView dequeueReusableCellWithIdentifier:forIndexPath:]因为这会导致有趣的问题.)当你这样做时,你在IB中定义的所有元素都将被实例化.


Moh*_*asm 15

动态表视图单元格高度和自动布局

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

- (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;
}
Run Code Online (Sandbox Code Playgroud)

  • 是的,我知道......但是我不想使用PureLayout,而dispatch_once'技巧'对我帮助很大,只能使用Storyboard来解决它. (2认同)

alp*_*com 13

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

这对我来说是一个痛苦的"解决方案",因为他总共投入了20多个非常令人沮丧的小时,试图构建像smileyborg建议的那样,并且失败了几个月和三个版本的App Store版本.

我的看法是,如果你真的需要iOS 7的支持(对我们来说,它是必不可少的)那么这项技术太脆弱了你会把你的头发拉出去试试.除非你使用一些高级行编辑功能和/或真的需要支持1000多个"行"(在我们的应用程序中,它实际上从不超过20行),UITableView通常是完全矫枉过正的.

额外的好处是,与UITableView附带的所有委托垃圾和来回相比,代码变得非常简单.它只是viewOnLoad中的一个代码循环,看起来很优雅,易于管理.

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

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

2)将UIScrollView拖到根视图上.

3)将顶部,底部,左侧和右侧约束的约束添加到顶级视图,以便UIScrollView填充整个根视图.

4)在UIScrollView中添加一个UIView并将其命名为"container".将顶部,底部,左侧和右侧约束添加到UIScrollView(其父级).KEY TRICK:还添加"Equal widths"约束来链接UIScrollView和UIView.

您将收到错误"滚动视图具有不明确的可滚动内容高度",并且您的容器UIView应该具有0像素的高度.当应用程序运行时,这两个错误似乎都不重要.

5)为每个"单元"创建nib文件和控制器.使用UIView而不是UITableViewCell.

5)在你的根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!)
    }
}
Run Code Online (Sandbox Code Playgroud)

这是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))
    }
}
Run Code Online (Sandbox Code Playgroud)

到目前为止,我用这种方法找到的唯一"问题"是UITableView在滚动时在视图顶部有一个很好的"浮动"节标题功能.上面的解决方案不会这样做,除非你添加更多的编程,但对于我们的特殊情况,这个功能并非100%必不可少,当它消失时没有人注意到.

如果你想要在单元格之间使用分隔线,只需在自定义"单元格"底部添加1像素高的UIView,它看起来像一个分隔线.

确保打开"弹跳"和"垂直弹跳"以使刷新控件正常工作,因此它看起来更像是一个tableview.

TableView显示了您的内容下的一些空行和分隔符,如果它没有填满整个屏幕,因为此解决方案没有.但就个人而言,我更喜欢那些空行还是不存在 - 随着可变单元高度的变化,无论如何我总是看起来"马车",因为那里有空行.

这里希望其他一些程序员在阅读我的帖子之前浪费20多个小时试图用他们自己的应用程序中的Table View来解决它.:)


Abo*_*tef 12

tableView.estimatedRowHeight = 343.0
tableView.rowHeight = UITableViewAutomaticDimension
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述


Man*_*lan 11

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

然后我补充说

[cell layoutSubviews];
Run Code Online (Sandbox Code Playgroud)

在执行之前

[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
Run Code Online (Sandbox Code Playgroud)

在该标签的宽度与预期之后,动态高度计算正确.


Vad*_*off 8

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

1)将子视图的底部约束设置为等于cell.contentView减去所需的填充.不要在单元格或cell.contentView本身上设置约束.

2)将任的tableView的rowHeight财产或tableView:heightForRowAtIndexPath:UITableViewAutomaticDimension.

3)设置tableView的estimatedRowHeight属性或tableView:estimatedHeightForRowAtIndexPath:高度的最佳猜测.

而已.


Jam*_*run 5

如果您以编程方式进行布局,则以下是在Swift中使用锚点为iOS 10考虑的问题。

共有三个规则/步骤

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

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100
Run Code Online (Sandbox Code Playgroud)

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

        ])
}
Run Code Online (Sandbox Code Playgroud)

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

第3点:请勿拨打电话:

  override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    }
Run Code Online (Sandbox Code Playgroud)

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

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

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