在UITableView中使用自动布局来实现dynamic单元格布局和可变行高
如何在表格视图中使用UITableViewCell
的自动布局,让每个单元格的内容和子视图决定行高度(本身/自动),同时保持平滑的滚动性能?
TL; DR:不喜欢读书? 直接跳转到GitHub上的示例项目:
- iOS 8示例项目 – 需要iOS 8
- iOS 7示例项目 – 适用于iOS 7+
概念描述
无论您正在开发哪个iOS版本,前两个步骤都适用。
1.设置和添加约束
在您的UITableViewCell
子类中,添加约束,以便单元格的子视图将其边缘固定到单元格的contentView的边缘(最重要的是顶部和底部边缘)。 注意:不要将子视图固定到单元格本身; 只对contentView
的contentView
! 通过确保每个子视图的垂直维度中的内容压缩抵抗和内容拥抱约束不会被您添加的更高优先级的约束覆盖,让这些子视图的内在内容大小驱动表视图单元格的内容视图的高度。 ( 嗯?点击这里 )
记住,这个想法是让单元格的子视图垂直连接到单元格的内容视图,这样他们就可以“施加压力”并使内容视图展开以适应它们。 使用一个带有几个子视图的示例单元格,下面是一些视图,说明一些 (不是全部)你的约束需要看起来像:
你可以想像,随着更多的文本被添加到上面的示例单元格中的多行身体标签,它将需要垂直增长以适应文本,这将有效地强制单元格高度增长。 (当然,为了正确工作,你需要正确的得到约束!)
正确获取约束无疑是获得dynamic单元高度与自动布局一起工作的最难和最重要的部分 。 如果你在这里犯了一个错误,它可能会阻止所有其他的工作 – 所以请花时间! 我build议在代码中设置你的约束,因为你确切地知道哪些约束被添加到哪里,并且在出错的时候更容易debugging。 在代码中添加约束与使用其中一个可用的开放源代码API的界面生成器一样简单,而且function也要强大 – 以下是我devise,维护和使用的一个: https : //github.com/ smileyborg / PureLayout
- 如果您在代码中添加约束,您应该从UITableViewCell子类的
updateConstraints
方法中执行一次。 请注意,可能会多次调用updateConstraints
,因此为了避免多次添加相同的约束,请确保在updateConstraints
中包含约束添加代码,以检查布尔属性,如didSetupConstraints
(在您之后设置为YES运行你的约束添加代码一次)。 另一方面,如果你有更新现有约束的代码(比如调整一些约束的constant
属性),把它放在updateConstraints
但在didSetupConstraints
的检查之外,这样它就可以在每次调用方法时运行。
2.确定唯一的表格视图单元重用标识符
对于单元中的每个唯一约束集,请使用唯一的单元重用标识符。 换句话说,如果你的单元格有多个唯一的布局,每个唯一的布局应该接收它自己的重用标识符。 (一个很好的提示,你需要使用一个新的重用标识符是当你的单元格变体具有不同数量的子视图,或者子视图以不同的方式排列时。
例如,如果您在每个单元格中显示电子邮件,则可能有4种独特的布局:仅包含主题的邮件,包含主题和正文的邮件,包含主题和照片附件的邮件以及包含主题的邮件,身体和照片附件。 每个布局都有完全不同的约束条件来实现它,所以一旦单元格被初始化并且为这些单元格types添加约束条件,单元格应该得到一个特定于该单元格types的唯一的重用标识符。 这意味着,当您将某个单元出列以供重用时,约束条件已被添加并准备好用于该单元格types。
请注意,由于内在内容大小的差异,具有相同约束(types)的单元格可能仍然具有不同的高度! 不要混淆不同的布局(不同的约束)与不同的计算视图框架(由相同的约束解决)由于不同的内容大小。
- 不要将具有完全不同的约束集合的单元添加到相同的重用池(即使用相同的重用标识符),然后尝试从每个出列后的旧约束中删除旧约束并设置新约束。 内部的自动布局引擎不是为处理大规模的约束变化而devise的,您将看到大量的性能问题。
对于iOS 8 – 自我大小的单元格
3.启用行高估计
对于iOS 8,Apple已经内化了之前iOS 8以前必须执行的大部分工作。为了允许自定义大小的单元格机制工作,您必须首先将table view的rowHeight
属性设置为常量UITableViewAutomaticDimension
。 然后,您只需要通过将表视图的estimatedRowHeight
属性设置为非零值来启用行高度估计,例如:
self.tableView.rowHeight = UITableViewAutomaticDimension; self.tableView.estimatedRowHeight = 44.0; // set to whatever your "average" cell height is
这样做是为表格视图提供临时估计/占位符,用于未在屏幕上的单元格的行高度。 然后,当这些单元格要在屏幕上滚动时,将计算实际的行高。 为了确定每一行的实际高度,表视图会自动询问每个单元格的内容视图需要基于内容视图的已知固定宽度(基于表视图的宽度,减去任何其他内容)索引或附件视图)以及添加到单元格内容视图和子视图的自动布局约束。 一旦这个实际的单元格高度已经确定,行的旧的估计高度更新为新的实际高度(并根据需要对表视图的contentSize / contentOffset进行任何调整)。
一般来说,您所提供的估算值并不一定非常准确 – 只用于在表格视图中正确调整滚动指标的大小,而且表格视图可以很好地调整滚动指标,从而获得错误的估计值在屏幕上滚动单元格。 您应该将表视图(以viewDidLoad
或类似方式)的estimatedRowHeight
属性设置为“平均”行高的常量值。 只有当你的行高有极大的可变性(例如相差一个数量级),你注意到滚动指标“跳”滚动时,你应该打扰实施tableView:estimatedHeightForRowAtIndexPath:
做最小的计算所需返回一个更准确的估计每一行。
对于iOS 7支持(自己实现自动resize)
3.做一个布局通过并获取单元格高度
首先,实例化一个表视图单元格的屏幕外实例, 每个重用标识符一个实例,严格用于高度计算。 (离屏表示单元格引用存储在视图控制器上的属性/ ivar上,并且从未从tableView:cellForRowAtIndexPath:
返回tableView:cellForRowAtIndexPath:
表格视图实际上在屏幕上呈现。)接下来,必须为单元格configuration确切的内容(例如文本,图像等),如果它将被显示在表格视图中,它将保持。
然后,强制单元立即布局它的子视图,然后使用UITableViewCell
的contentView
上的systemLayoutSizeFittingSize:
方法来找出单元所需的高度。 使用UILayoutFittingCompressedSize
获取所需的最小大小以适应单元格的所有内容。 然后可以从tableView:heightForRowAtIndexPath:
delegate方法返回高度。
4.使用估计的行高
如果你的表视图有多于几十行,你会发现,执行自动布局约束求解可能会在第一次加载表视图时快速陷入主线程,因为每行都调用tableView:heightForRowAtIndexPath:
在第一次加载时(为了计算滚动指示器的大小)。
从iOS 7开始,您可以(也绝对应该)在表视图上使用estimatedRowHeight
属性。 这样做是为表格视图提供临时估计/占位符,用于未在屏幕上的单元格的行高度。 然后,当这些单元格要在屏幕上滚动时,将计算实际的行高(通过调用tableView:heightForRowAtIndexPath:
,并将估计的高度更新为实际高度。
一般来说,您所提供的估算值并不一定非常准确 – 只用于在表格视图中正确调整滚动指标的大小,而且表格视图可以很好地调整滚动指标,从而获得错误的估计值在屏幕上滚动单元格。 您应该将表视图(以viewDidLoad
或类似方式)的estimatedRowHeight
属性设置为“平均”行高的常量值。 只有当你的行高有极大的可变性(例如相差一个数量级),你注意到滚动指标“跳”滚动时,你应该打扰实施tableView:estimatedHeightForRowAtIndexPath:
做最小的计算所需返回一个更准确的估计每一行。
5.(如果需要)添加行高度caching
如果你已经完成了上述所有工作,并且仍然发现在tableView:heightForRowAtIndexPath:
执行约束求解时性能是无法接受的,那么你不幸的是需要为单元高度实现一些caching。 (这是苹果工程师build议的方法)。总体思路是让Auto Layout引擎第一次解决约束,然后caching该单元格的计算高度,并将该caching值用于该单元格高度的所有未来请求。 诀窍当然是确保在发生任何可能导致单元格高度发生变化的事情时清除单元格的高速caching高度 – 主要是当单元格的内容发生更改或发生其他重要事件时(如用户调整dynamictypes文字大小滑块)。
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.0f, 0.0f, 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.0f; 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.0f; } else { return 40.0f; } }
示例项目
- iOS 8示例项目 – 需要iOS 8
- iOS 7示例项目 – 适用于iOS 7+
这些项目是由于在UILabels中包含dynamic内容的表视图单元格而具有可变行高的表视图的完整工作示例。
随时提出您遇到的任何问题或问题(您可以在GitHub上打开问题或在这里发表评论)。 我会尽力帮助!
Xamarin(C#/。NET)
如果您使用的是Xamarin,请查看@KentBoogaart放在一起的示例项目 。
对于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 }
重要
-
如果有多行标签,不要忘记将
numberOfLines
设置为0
。 -
不要忘了
label.preferredMaxLayoutWidth = CGRectGetWidth(tableView.bounds)
完整的示例代码在这里 。
一个可变高度的UITableViewCell的Swift示例
更新了Swift 3
William Hu的Swift答案很好,但是当我第一次学习做某事的时候,它可以帮助我做一些简单而又细致的步骤。 下面的例子是我的testing项目,同时学习使可变单元高度的UITableView
。 我基于这个基本的Swift的UITableView示例 。
完成的项目应该是这样的:
创build一个新的项目
它可以只是一个单一的视图应用程序。
添加代码
将一个新的Swift文件添加到您的项目。 将其命名为MyCustomCell。 本课将为您添加到故事板中单元格的视图设置分支。 在这个基本的例子中,我们将只在每个单元格中有一个标签。
import UIKit class MyCustomCell: UITableViewCell { @IBOutlet weak var myCellLabel: UILabel! }
我们稍后会连接这个sockets。
打开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
设置故事板
添加一个表视图到您的视图控制器,并使用自动布局将其钉在四边。 然后将表格视图单元格拖到表格视图上。 然后在Prototype单元格上拖动一个Label。 使用自动布局将标签固定到表格视图单元格的内容视图的四个边缘。
重要的提示:
- 自动布局与上面提到的重要的两行代码一起工作。 如果你不使用自动布局,它不会工作。
其他IB设置
自定义类名和标识符
selectTable View Cell并将自定义类设置为MyCustomCell
(我们添加的Swift文件中的类的名称)。 还要将标识符设置为cell
(与上面代码中cellReuseIdentifier
所用的string相同)。
标签的零线
将标签中的行数设置为0
。 这意味着多行,并允许标签根据其内容调整自己的大小。
连接sockets
- 控制从Storyboard中的Table View拖动到
ViewController
代码中的tableView
variables。 - 对Prototype单元格中的Label执行
myCellLabel
类中的myCellLabel
variables。
成品
你现在应该可以运行你的项目,并获得可变高度的单元格。
笔记
- 这个例子只适用于iOS 8以及之后。 如果你仍然需要支持iOS 7,那么这不适合你。
- 您未来项目中的自定义单元可能不止一个标签。 确保你把所有东西都固定好了,这样自动布局就可以决定使用的正确高度。 您可能还必须使用垂直压缩阻力和拥抱。 请参阅这篇文章了解更多。
-
如果您不固定前导和尾随(左侧和右侧)边缘,则可能还需要设置标签的
preferredMaxLayoutWidth
以便知道何时换行。 例如,如果您已经在上面的项目的标签中添加了一个Center Horizontally约束,而不是固定前导和后沿,那么您需要将此行添加到tableView:cellForRowAtIndexPath
方法中:cell.myCellLabel.preferredMaxLayoutWidth = tableView.bounds.width
也可以看看
- 了解iOS 8中的自定义大小和dynamictypes
- 具有不同行高的表视图单元格
- Swift的UITableView示例
我把@ smileyborg的iOS7解决scheme包装在一个类别中
我决定把@smileyborg这个聪明的解决scheme包装成一个UICollectionViewCell+AutoLayoutDynamicHeightCalculation
类别。
该类别还纠正@ wildmonkey的答案中概述的问题(从nib和systemLayoutSizeFittingSize:
加载单元格systemLayoutSizeFittingSize:
返回CGRectZero
)
它没有考虑任何caching,但现在适合我的需求。 随意复制,粘贴和破解它。
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中做这个爵士乐,但是现在呢!
这是我的解决scheme。 在加载视图之前,您需要告诉TableView估计的高度。 否则,它不会像预期的那样行事。
- (void)viewWillAppear:(BOOL)animated { _messageField.delegate = self; _tableView.estimatedRowHeight = 65.0; _tableView.rowHeight = UITableViewAutomaticDimension; }
@smileyborg提出的解决scheme几乎是完美的。 If you have a custom cell and you want one or more UILabel
with dynamic heights then the systemLayoutSizeFittingSize method combined with AutoLayout enabled returns a CGSizeZero
unless you move all your cell constraints from the cell to its contentView (as suggested by @TomSwift here How to resize superview to fit all subviews with autolayout? ).
To do so you need to insert the following code in your custom UITableViewCell implementation (thanks to @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]; } }
Mixing @smileyborg answer with this should works.
An important enough gotcha I just ran into to post as an answer.
@smileyborg's answer is mostly correct. However, if you have any code in the layoutSubviews
method of your custom cell class, for instance setting the preferredMaxLayoutWidth
, then it won't be run with this code:
[cell.contentView setNeedsLayout]; [cell.contentView layoutIfNeeded];
It confounded me for awhile. Then I realized it's because those are only triggering layoutSubviews on the contentView
, not the cell itself.
My working code looks like this:
TCAnswerDetailAppSummaryCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TCAnswerDetailAppSummaryCell"]; [cell configureWithThirdPartyObject:self.app]; [cell layoutIfNeeded]; CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; return height;
Note that if you are creating a new cell, I'm pretty sure you don't need to call setNeedsLayout
as it should already be set. In cases where you save a reference to a cell, you should probably call it. Either way it shouldn't hurt anything.
Another tip if you are using cell subclasses where you are setting things like preferredMaxLayoutWidth
. As @smileyborg mentions, "your table view cell hasn't yet had its width fixed to the table view's width". This is true, and trouble if you are doing your work in your subclass and not in the view controller. However you can simply set the cell frame at this point using the table width:
For instance in the calculation for height:
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);
(I happen to cache this particular cell for re-use, but that's irrelevant).
In case people are still having trouble with this. I wrote a quick blog post about using Autolayout with UITableViews Leveraging Autolayout For Dynamic Cell Heights as well as an open source component to help make this more abstract and easier to implement. https://github.com/Raizlabs/RZCellSizeManager
As long as your layout in your cell is good.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath]; return [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height; }
Update: You should use dynamic resizing introduced in iOS 8.
(for Xcode 8.x / Xcode 9.x read at the bottom)
Beware of the following issue in in Xcode 7.x, which might be a source of confusion:
Interface Builder does not handle auto-sizing cell set-up properly. Even if your constraints are absolutely valid, IB will still complain and give you confusing suggestions and errors. The reason is that IB is unwilling to change the row's height as your constraints dictate (so that the cell fits around your content). Instead, it keeps the row's height fixed and starts suggesting you change your constraints, which you should ignore .
For example, imagine you've set up everything fine, no warnings, no errors, all works.
Now if you change the font size (in this example I'm changing the description label font size from 17.0 to 18.0).
Because the font size increased, the label now wants to occupy 3 rows (before that it was occupying 2 rows).
If Interface Builder worked as expected, it would resize the cell's height to accommodate the new label height. However what actually happens is that IB displays the red auto-layout error icon and suggest that you modify hugging/compression priorities.
You should ignore these warnings. What you can* do instead is to manually change the row's height in (select Cell > Size Inspector > Row Height).
I was changing this height one click at a time (using the up/down stepper) until the red arrow errors disappear! (you will actually get yellow warnings, at which point just go ahead and do 'update frames', it should all work).
* Note that you don't actually have to resolve these red errors or yellow warnings in Interface Builder – at runtime, everything will work correctly (even if IB shows errors/warnings). Just make sure that at runtime in the console log you're not getting any AutoLayout errors.
In fact trying to always update row height in IB is super annoying and sometimes close to impossible (because of fractional values).
To prevent the annoying IB warnings/errors, you can select the views involved and in Size Inspector
for the property Ambiguity
choose Verify Position Only
Xcode 8.x / Xcode 9.x seems to (sometimes) be doing things differently than Xcode 7.x, but still incorrectly. For example even when compression resistance priority
/ hugging priority
are set to required (1000), Interface Builder might stretch or clip a label to fit the cell (instead of resizing cell height to fit around the label). And in such a case it might not even show any AutoLayout warnings or errors. Or sometimes it does exactly what Xcode 7.x did, described above.
Like @Bob-Spryn I ran into an important enough gotcha that I'm posting this as an answer.
I struggled with @smileyborg's answer for a while. The gotcha that I ran into is if you've defined your prototype cell in IB with additional elements ( UILabels
, UIButtons
, etc.) in IB when you instantiate the cell with [ [YourTableViewCellClass alloc] init]
it will not instantiate all the other elements within that cell unless you've written code to do that. (I had a similar experience with initWithStyle
.)
To have the storyboard instantiate all the additional elements obtain your cell with [tableView dequeueReusableCellWithIdentifier:@"DoseNeeded"]
(Not [tableView dequeueReusableCellWithIdentifier:forIndexPath:]
as this'll cause interesting problems.) When you do this all the elements you defined in IB will be instantiated.
Dynamic Table View Cell Height and Auto Layout
A good way to solve the problem with storyboard Auto Layout:
- (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; }
Another "solution": skip all this frustration and use a UIScrollView instead to get a result that looks and feels identical to UITableView.
That was the painful "solution" for me, after having put in literally 20+ very frustrating hours total trying to build something like what smileyborg suggested and failing over many months and three versions of App Store releases.
My take is that if you really need iOS 7 support (for us, it's essential) then the technology is just too brittle and you'll pull your hair out trying. And that UITableView is complete overkill generally unless you're using some of the advanced row editing features and/or really need to support 1000+ "rows" (in our app, it's realistically never more than 20 rows).
The added bonus is that the code gets insanely simple versus all the delegate crap and back and forth that comes with UITableView. It's just one single loop of code in viewOnLoad that looks elegant and is easy to manage.
Here's some tips on how to do it:
1) Using either Storyboard or a nib file, create a ViewController and associated root view.
2) Drag over a UIScrollView onto your root view.
3) Add constraints top, bottom, left and right constraints to the top-level view so the UIScrollView fills the entire root view.
4) Add a UIView inside the UIScrollView and call it "container". Add top, bottom, left and right constraints to the UIScrollView (its parent). KEY TRICK: Also add a "Equal widths" constraints to link the UIScrollView and UIView.
You will get an error "scroll view has ambiguous scrollable content height" and that your container UIView should have a height of 0 pixels. Neither error seems to matter when the app is running.
5) Create nib files and controllers for each of your "cells". Use UIView not UITableViewCell.
5) In your root ViewController, you essentially add all the "rows" to the container UIView and programmatically add constraints linking their left and right edges to the container view, their top edges to either the container view top (for the first item) or the previous cell. Then link the final cell to the container bottom.
For us, each "row" is in a nib file. So the code looks something like this:
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!) } }
And here's the code for 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)) } }
The only "gotcha" I've found with this approach so far is that UITableView has a nice feature of "floating" section headers at the top of the view as you scroll. The above solution won't do that unless you add more programming but for our particular case this feature wasn't 100% essential and nobody noticed when it went away.
If you want dividers between your cells, just add a 1 pixel high UIView at the bottom of your custom "cell" that looks like a divider.
Be sure to turn on "bounces" and "bounce vertically" for the refresh control to work and so it seems more like a tableview.
TableView shows some empty rows and dividers under your content, if it doesn't fill the full screen where as this solution doesn't. But personally, I prefer if those empty rows weren't there anyway – with variable cell height it always looked "buggy" to me anyway to have the empty rows in there.
Here's hoping some other programmer reads my post BEFORE wasting 20+ hours trying to figure it out with Table View in their own app. 🙂
tableView.estimatedRowHeight = 343.0 tableView.rowHeight = UITableViewAutomaticDimension
I had to use dynamic views (setup views and constraints by code) and when I wanted to set preferredMaxLayoutWidth label's width was 0. So I've got wrong cell height.
Then I added
[cell layoutSubviews];
before executing
[cell setNeedsUpdateConstraints]; [cell updateConstraintsIfNeeded];
After that label's width was as expected and dynamic height was calculating right.
Let's say you have a cell with a subview, and you want the cell's height to be high enough to encompass the subview + padding.
1) Set the subview's bottom constraint equal to the cell.contentView minus the padding you want. Do not set constraints on the cell or cell.contentView itself.
2) Set either the tableView's rowHeight
property or tableView:heightForRowAtIndexPath:
to UITableViewAutomaticDimension
.
3) Set either the tableView's estimatedRowHeight
property or tableView:estimatedHeightForRowAtIndexPath:
to a best guess of the height.
而已。
I've also found a YouTube video explaining how to achieve this using the STV framework .
If you do you layout programmatically, here is what to consider for iOS 10 using anchors in Swift.
There are three rules/ steps
NUMBER 1: set this two properties of tableview on viewDidLoad, the first one is telling to the tableview that should expect dynamic sizes on their cells, the second one is just to let the app calculate the size of the scrollbar indicator, so it helps for performance.
tableView.rowHeight = UITableViewAutomaticDimension tableView.estimatedRowHeight = 100
NUMBER 2: This is important you need to add the subviews to the contentView of the cell not to the view, and also use its layoutsmarginguide to anchor the subviews to the top and bottom, this is a working example of how to do it.
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) ]) }
Create a method that will add the subviews and perform the layout, call it in the init method.
NUMBER 3: DON'T CALL THE METHOD:
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { }
If you do it you will override your implementation.
Follow this 3 rules for dynamic cells in tableviews.
here is a working implementation https://github.com/jamesrochabrun/MinimalViewController
In my case i have to create a custom cell with a image which is coming from server and can be of any width and height. And two UILabels with dynamic size(both width & height)
i have achieved the same here in my answer with autolayout and programmatically:
Basically above @smileyBorg answer helped but systemLayoutSizeFittingSize never worked for me, In my approach :
1. No use of automatic row height calculation property. 2.No use of estimated height 3.No need of unnecessary updateConstraints. 4.No use of Automatic Preferred Max Layout Width. 5. No use of systemLayoutSizeFittingSize (should have use but not working for me, i dont know what it is doing internally), but instead my method -(float)getViewHeight working and i know what it's doing internally.
Is it possible to have differing heights in a UITableView Cell when I use several different ways of displaying the cell?
In my case, the padding was because of the sectionHeader and sectionFooter heights, where storyboard allowed me to change it to minimum 1. So in viewDidLoad method:
tableView.sectionHeaderHeight = 0 tableView.sectionFooterHeight = 0
To set automatic dimension for row height & estimated row height, ensure following steps to make, auto dimension effective for cell/row height layout.
- Assign and implement dataSource and delegate
- Assign
UITableViewAutomaticDimension
to rowHeight & estimatedRowHeight - Implement delegate/dataSource methods (ie
heightForRowAt
and return a valueUITableViewAutomaticDimension
to it)
–
@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 table.rowHeight = UITableViewAutomaticDimension table.estimatedRowHeight = UITableViewAutomaticDimension } // UITableViewAutomaticDimension calculates height of label contents/text func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return UITableViewAutomaticDimension }
For label instance in UITableviewCell
- Set number of lines = 0 (& line break mode = truncate tail)
- Set all constraints (top, bottom, right left) with respect to its superview/ cell container.
- Optional : Set minimum height for label, if you want minimum vertical area covered by label, even if there is no data.
Have tried:
func textViewDidChange(_ textView: UITextView) { var frame = textView.frame frame.size.height = textView.contentSize.height textView.frame = frame print(frame) self.tableView.rowHeight = frame.size.height
but although I get the right frame height the row does not adjust
yet another iOs7+iOs8 solution in Swift
var cell2height:CGFloat=44 override func viewDidLoad() { super.viewDidLoad() theTable.rowHeight = UITableViewAutomaticDimension theTable.estimatedRowHeight = 44.0; } func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCellWithIdentifier("myTableViewCell", forIndexPath: indexPath) as! myTableViewCell cell2height=cell.contentView.height return cell } func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat { if #available(iOS 8.0, *) { return UITableViewAutomaticDimension } else { return cell2height } }