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


当前回答

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

分配和实现表视图数据源和委托将UITableViewAutomaticDimension分配给rowHeight和estimatedRowHeight实现委托/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;
}

斯威夫特:

@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(换行模式=截断尾部(&L)设置与其超级视图/单元格容器相关的所有约束(顶部、底部、左右)。可选:如果您希望标签覆盖最小垂直区域,即使没有数据,也可以设置标签的最小高度。

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

其他回答

@smileyborg提出的解决方案几乎是完美的。如果您有一个自定义单元格,并且需要一个或多个具有动态高度的UILabel,则systemLayoutSizeFittingSize方法与启用自动布局相结合将返回一个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的iOS7解决方案归入一个类别

我决定用@smileyborg将这个聪明的解决方案包装成UICollectionViewCell+AutoLayoutDynamicHeightCalculation类别。

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

它不考虑任何缓存,但现在适合我的需要。随意复制、粘贴和破解。

UI集合视图单元格+自动布局动态高度计算.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

UI集合视图单元格+自动布局动态高度计算.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中演奏爵士乐,但现在就可以了!

(对于底部读取的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显示错误/警告)。只需确保在运行时控制台日志中没有出现任何自动布局错误。事实上,尝试总是更新IB中的行高度是非常令人讨厌的,有时几乎不可能(因为分数值)。为了防止烦人的IB警告/错误,您可以选择所涉及的视图,并在Size Inspector中为属性Ambiguity选择Verify Position Only


Xcode 8.x/Xcode 9.x看起来(有时)做的事情与Xcode 7.x不同,但仍然不正确。例如,即使抗压优先级/拥抱优先级设置为必需(1000),Interface Builder也可能拉伸或剪裁标签以适合单元格(而不是调整单元格高度以适合标签周围)。在这种情况下,它甚至可能不会显示任何自动布局警告或错误。或者,有时它完全像上面描述的Xcode7.x那样。

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

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

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

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

有三个规则/步骤

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

    tableView.rowHeight = UITableViewAutomaticDimension
    tableView.estimatedRowHeight = 100

第二:这一点很重要。您需要将子视图添加到单元格的contentView中,而不是添加到视图中,还需要使用其layoutsmargingguide将子视图锚定到顶部和底部,这是一个很好的示例。

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