为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

iOS
梵蒂冈之花 2019-05-22 13:38:21
在UITableView中使用自动布局来获取动态单元格布局和可变行高如何UITableViewCell在表格视图中使用s 内的自动布局,让每个单元格的内容和子视图确定行高(本身/自动),同时保持平滑的滚动性能?
查看完整描述

4 回答

?
holdtom

TA贡献1805条经验 获得超10个赞

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

这样做是为表视图提供临时估计/占位符,用于尚未在屏幕上显示的单元格的行高。然后,当这些单元格即将在屏幕上滚动时,将计算实际行高。要确定每一行的实际高度,表视图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 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


查看完整回答
反对 回复 2019-05-22
?
拉风的咖菲猫

TA贡献1995条经验 获得超2个赞

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

重要

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

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

完整的示例代码在这里

编辑 Swift 4.2 UITableViewAutomaticDimension改为 UITableView.automaticDimension


查看完整回答
反对 回复 2019-05-22
?
大话西游666

TA贡献1817条经验 获得超14个赞

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


查看完整回答
反对 回复 2019-05-22
?
慕村225694

TA贡献1880条经验 获得超4个赞

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

Objective-C的

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

更新到Swift 4.2

override func viewWillAppear(_ animated: Bool) {
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 65.0}


查看完整回答
反对 回复 2019-05-22
  • 4 回答
  • 0 关注
  • 960 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信