ios - 在 UITableView 中使用自动布局进行动态单元格布局和可变行高

标签 ios uitableview autolayout row-height

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

最佳答案

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

  • iOS 8 Sample Project - 需要 iOS 8
  • iOS 7 Sample Project - 适用于 iOS 7+

  • 概念描述
    无论您为哪个 iOS 版本开发,下面的前 2 个步骤都适用。
    1. 设置和添加约束
    在您的 UITableViewCell子类,添加约束,以便单元格的 subview 的边缘固定到单元格的边缘 内容查看 (最重要的是顶部和底部边缘)。 注意:不要将 subview 固定到单元格本身;仅到单元格的 contentView ! 通过确保每个 subview 的垂直维度的内容压缩阻力和内容拥抱约束不会被您添加的更高优先级约束覆盖,让这些 subview 的内在内容大小驱动表格 View 单元格内容 View 的高度。 ( Huh? Click here. )
    请记住,这个想法是让单元格的 subview 垂直连接到单元格的内容 View ,以便它们可以“施加压力”并使内容 View 扩展以适应它们。使用带有几个 subview 的示例单元格,这是 的直观说明一些 (不是全部!)你的约束需要看起来像:
    Example illustration of constraints on a table view cell.
    您可以想象,随着更多文本添加到上面示例单元格中的多行正文标签,它需要垂直增长以适应文本,这将有效地迫使单元格的高度增长。 (当然,您需要正确设置约束才能使其正常工作!)
    正确设置约束绝对是 最难最重要的部分使用自动布局获取动态单元格高度的方法。如果你在这里犯了一个错误,它可能会阻止其他一切工作——所以慢慢来!我建议在代码中设置你的约束,因为你确切地知道在何处添加了哪些约束,并且在出现问题时更容易调试。在代码中添加约束与使用布局 anchor 的 Interface Builder 或 GitHub 上可用的出色开源 API 之一一样简单,而且功能明显更强大。
  • 如果你在代码中添加约束,你应该在 updateConstraints 中做一次。 UITableViewCell 子类的方法。请注意 updateConstraints可能会被多次调用,因此为了避免多次添加相同的约束,请确保将添加约束的代码包装在 updateConstraints 内。检查 bool 属性,例如 didSetupConstraints (您在运行一次添加约束的代码后将其设置为 YES)。另一方面,如果您有更新现有约束的代码(例如调整某些约束的 constant 属性),请将其放在 updateConstraints 中。但在 didSetupConstraints 的检查之外因此它可以在每次调用该方法时运行。

  • 2. 确定唯一的表 View 单元重用标识符
    对于单元中每组唯一的约束,使用唯一的单元重用标识符。换句话说,如果您的单元格有多个独特的布局,则每个独特的布局都应该收到自己的重用标识符。 (当您的单元格变体具有不同数量的 subview ,或者 subview 以不同的方式排列时,您需要使用新的重用标识符的一个很好的提示。)
    例如,如果您在每个单元格中显示一封电子邮件,您可能有 4 种独特的布局:只有一个主题的邮件、一个主题和一个正文的邮件、一个主题和一个照片附件的邮件以及一个主题的邮件, body 和照片附件。每个布局都有完全不同的约束来实现它,因此一旦单元被初始化并为这些单元类型之一添加了约束,单元应该获得特定于该单元类型的唯一重用标识符。这意味着当您将单元出列以供重用时,约束已经添加并准备好用于该单元类型。
    请注意,由于内在内容大小的差异,具有相同约束(类型)的单元格可能仍具有不同的高度!由于内容的大小不同,不要将根本不同的布局(不同的约束)与不同的计算 View 框架(从相同的约束解决)混淆。
  • 不要将具有完全不同约束集的单元添加到同一个重用池中(即使用相同的重用标识符),然后在每次出队后尝试删除旧约束并从头开始设置新约束。内部自动布局引擎不是为了处理约束的大规模变化而设计的,你会看到大量的性能问题。

  • 对于 iOS 8 - 调整单元格大小
    3. 启用行高估计

    To enable self-sizing table view cells, you must set the table view’s rowHeight property to UITableViewAutomaticDimension. You must also assign a value to the estimatedRowHeight property. As soon as both of these properties are set, the system uses Auto Layout to calculate the row’s actual height

    Apple: Working with Self-Sizing Table View Cells


    在 iOS 8 中,Apple 已经内化了之前必须由您在 iOS 8 之前实现的大部分工作。为了让自调整单元格机制起作用,您必须首先设置 rowHeight。表 View 上的属性为常量 UITableView.automaticDimension .然后,您只需通过设置表 View 的 estimatedRowHeight 来启用行高估计。属性为非零值,例如:
    self.tableView.rowHeight = UITableView.automaticDimension;
    self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is
    
    这样做是为表格 View 提供一个临时估计/占位符,用于尚未出现在屏幕上的单元格的行高。然后,当这些单元格即将在屏幕上滚动时,将计算实际行高。为了确定每一行的实际高度,表格 View 会自动询问每个单元格的高度是多少 contentView需要基于内容 View 的已知固定宽度(基于表格 View 的宽度,减去任何其他内容,如部分索引或附件 View )以及您添加到单元格内容 View 和 subview 的自动布局约束.一旦确定了该实际单元格高度,该行的旧估计高度将更新为新的实际高度(并且根据您的需要对表格 View 的 contentSize/contentOffset 进行任何调整)。
    一般来说,您提供的估计不必非常准确——它仅用于正确调整表格 View 中滚动指示器的大小,并且表格 View 可以很好地调整滚动指示器以适应不正确的估计在屏幕上滚动单元格。您应该设置 estimatedRowHeight表 View (在 viewDidLoad 或类似的)上的属性为一个常量值,即“平均”行高。只有当您的行高具有极大的可变性(例如,相差一个数量级)并且您在滚动时注意到滚动指示器“跳跃”时,您才应该费心实现 tableView:estimatedHeightForRowAtIndexPath:为每行返回更准确的估计所需的最少计算。
    对于 iOS 7 支持(自己实现自动单元格大小调整)
    3. 做一个布局传递并获取单元格高度
    首先,实例化一个表格 View 单元的屏幕外实例,每个重用标识符一个实例,严格用于高度计算。 (离屏意味着单元格引用存储在 View Controller 上的属性/ivar 中,并且永远不会从 tableView:cellForRowAtIndexPath: 返回,以便表格 View 实际呈现在屏幕上。)接下来,必须使用准确的内容(例如文本、图像)配置单元格等),如果它要显示在表格 View 中,它将保持不变。
    然后,强制单元格立即布局其 subview ,然后使用 systemLayoutSizeFittingSize: UITableViewCell上的方法的 contentView找出所需的单元格高度是多少。使用 UILayoutFittingCompressedSize以获得适合单元格所有内容所需的最小尺寸。然后可以从 tableView:heightForRowAtIndexPath: 返回高度委托(delegate)方法。
    4. 使用估计的行高
    如果你的 table view 有几十行,你会发现在第一次加载 table view 时,做 Auto Layout 约束求解会很快卡住主线程,如 tableView:heightForRowAtIndexPath:在第一次加载时对每一行调用(为了计算滚动指示器的大小)。
    从 iOS 7 开始,您可以(并且绝对应该)使用 estimatedRowHeight表 View 上的属性。这样做是为表格 View 提供一个临时估计/占位符,用于尚未出现在屏幕上的单元格的行高。然后,当这些单元格即将在屏幕上滚动时,将计算实际行高(通过调用 tableView:heightForRowAtIndexPath: ),并使用实际行高更新估计高度。
    一般来说,您提供的估计不必非常准确——它仅用于在表格 View 中正确调整滚动指示器的大小,并且表格 View 可以很好地调整滚动指示器以适应不正确的估计在屏幕上滚动单元格。您应该设置 estimatedRowHeight表 View (在 viewDidLoad 或类似的)上的属性为一个常量值,即“平均”行高。只有当您的行高具有极大的可变性(例如,相差一个数量级)并且您在滚动时注意到滚动指示器“跳跃”时,您才应该费心实现 tableView:estimatedHeightForRowAtIndexPath:为每行返回更准确的估计所需的最少计算。
    5.(如果需要)添加行高缓存
    如果您已完成上述所有操作,但仍然发现在 tableView:heightForRowAtIndexPath: 中进行约束求解时性能低得令人无法接受。 ,不幸的是,您需要为单元格高度实现一些缓存。 (这是 Apple 工程师建议的方法。)一般的想法是让 Autolayout 引擎第一次解决约束,然后缓存该单元格的计算高度,并将缓存的值用于该单元格高度的所有 future 请求。当然,诀窍是确保在发生任何可能导致单元格高度发生变化的情况时清除单元格的缓存高度——主要是当该单元格的内容发生变化或其他重要事件发生时(例如用户调整动态类型文本大小滑块)。
    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;
        }
    }
    
    示例项目
  • iOS 8 Sample Project - 需要 iOS 8
  • iOS 7 Sample Project - 适用于 iOS 7+

  • 由于表格 View 单元格包含 UILabels 中的动态内容,这些项目是具有可变行高的表格 View 的完整工作示例。
    Xamarin (C#/.NET)
    如果您使用 Xamarin,请查看此 sample project@KentBoogaart 放在一起.

    关于ios - 在 UITableView 中使用自动布局进行动态单元格布局和可变行高,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18746929/

    相关文章:

    ios - 将 RestKit 日志发送到 Crashlytics

    ios - 如何在按下 searchBar 时显示 UISearchController 的 searchResultsController

    ios - UITableView 部分标题颜色行为不当

    ios - UITableHeaderView autoLayout 中的 UILabel 不适用于 iPhone 6

    ios - 如何在 ios 中以编程方式添加约束

    ios - 使用 NSDocumentDirectory 从 localPath 中删除每个单独的图像

    ios - 使用新的 Dropbox SDK (V2) 和 filesRoutes 的 downloadUrl :overwrite:destination: method 跟踪进度

    ios - UITableView 背景 View nil 在 iOS 5 中不起作用

    swift - UITableViewCell ImageView 总是显示不同的数据

    ios - 无法使用 AutoLayout 设置 UICollectionViewCell 内容的宽度