UICollectionView具有自动布局的自定义单元格

我正在尝试自动调整UICollectionViewCells与自动布局的工作,但我似乎无法让单元格的大小自己的内容。 我很难理解单元格的大小是如何从单元格内容的内容更新的。

这是我试过的设置:

  • 在contentView中使用UITextView自定义UICollectionViewCell。
  • 滚动的UITextView被禁用。
  • contentView的水平约束是:“H:| [_textView(320)]”,即UITextView固定在单元格的左侧,宽度为320。
  • contentView的垂直约束是:“V:| -0 – [_ textView]”,即UITextView固定在单元格的顶部。
  • UITextView有一个高度约束设置为一个常量,UITextView报告将适合文本。

以下是将单元格背景设置为红色并将UITextView背景设置为蓝色的样子: 细胞背景红色,UITextView背景蓝色

我把这个项目放在GitHub上。

更新了iOS 9

在iOS 9下看到我的GitHub解决scheme之后,我终于有时间充分调查问题了。 我现在已经更新了回购,以包括自定义单元格的不同configuration的几个例子。 我的结论是,自我测量的细胞在理论上是伟大的,但在实践中是混乱的。 在进行自我测量的细胞时要小心谨慎。

TL; DR

看看我的GitHub项目


自定义单元格只支持stream布局,所以请确保你正在使用。

有两件事你需要设置自我大小的细胞工作。

1.在UICollectionViewFlowLayout上设置estimatedItemSize

一旦设置了estimatedItemSize属性,stream布局将变成dynamic的。

 self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100) 

2.添加对您的单元格子类的大小的支持

这有两种口味。 自动布局自定义重写preferredLayoutAttributesFittingAttributes

使用自动布局创build和configuration单元格

我不会详细介绍这个,因为有一篇关于configuration单元约束的精彩SO贴子 。 只是要小心, Xcode 6打破了一堆与iOS 7的东西,所以,如果你支持iOS 7,你将需要做的东西,如确保autoresizingMask设置在单元格的contentView和contentView的边界被设置为单元格的边界时该单元格被加载(即awakeFromNib )。

你需要注意的是你的单元需要比Table View Cell更严格的约束。 例如,如果你希望你的宽度是dynamic的,那么你的单元需要一个高度约束。 同样,如果你想高度是dynamic的,那么你将需要一个宽度约束到你的单元格。

在自定义单元中实现preferredLayoutAttributesFittingAttributes

当这个函数被调用时,你的视图已经被configuration了内容(即cellForItem被调用)。 假设你的约束条件已经适当设置,你可以有这样的实现:

 //forces the system to do one layout pass var isHeightCalculated: Bool = false override func preferredLayoutAttributesFittingAttributes(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { //Exhibit A - We need to cache our calculation to prevent a crash. if !isHeightCalculated { setNeedsLayout() layoutIfNeeded() let size = contentView.systemLayoutSizeFittingSize(layoutAttributes.size) var newFrame = layoutAttributes.frame newFrame.size.width = CGFloat(ceilf(Float(size.width))) layoutAttributes.frame = newFrame isHeightCalculated = true } return layoutAttributes } 

注意在iOS 9上,如果您不小心,行为会发生一些变化,可能会导致您的实现崩溃(请参阅此处更多内容)。 在实现preferredLayoutAttributesFittingAttributes ,需要确保只更改一次布局属性的框架。 如果你不这样做,布局将无限期地调用你的实现,最终崩溃。 一种解决scheme是将计算的大小caching在单元格中,并在您重复使用单元格或更改其内容时使其无效,正如我对isHeightCalculated属性所做的isHeightCalculated

体验你的布局

在这一点上,你应该在你的collectionView中有“运转”的dynamic单元格。 我还没有find在我的testing过程中足够的开箱即用的解决scheme,所以如果您有任何意见,请随时发表评论。 它仍然感觉像UITableView赢得了dynamic的尺寸恕我直言的战斗。

注意事项

请注意,如果您正在使用原型单元格来计算estimatedItemSize – 如果您的XIB使用大小类别,则会中断。 原因是当你从XIB中加载你的单元格时,它的大小类将被configuration为Undefined 。 这只会在iOS 8上打破,因为在iOS 7上,大小类将根据设备(iPad = Regular-Any,iPhone = Compact-Any)加载。 您可以设置estimatedItemSize而不加载XIB,也可以从XIB加载单元格,将其添加到collectionView(这将设置traitCollection),执行布局,然后将其从超级视图中移除。 或者你也可以让你的单元覆盖traitCollection getter并返回适当的特性。 随你便。

让我知道如果我错过了什么,希望我帮助和好运编码


Daniel Galasso的答案的一些关键变化解决了我所有的问题。 不幸的是,我没有足够的声誉直接评论(还)。

在步骤1中,使用自动布局时,只需将单个父级UIView添加到单元格。 单元格内的所有内容必须是父级的子视图。 这回答了我所有的问题。 虽然XCode自动为UITableViewCells添加了这个,但它不会(但应该)为UICollectionViewCells。 根据文件 :

要configuration单元格的外观,请添加将contentView中的数据项目的内容作为子视图呈现给contentView属性中的视图所需的视图。 不要直接将子视图添加到单元格本身。

然后完全跳过第3步。 这是不需要的。

在iOS10中有一个新的常量叫做UICollectionViewFlowLayoutAutomaticSize ,所以相反:

 self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100) 

你可以使用这个:

 self.flowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 

它具有更好的性能,尤其是当您收集视图中的单元格具有常量wid时

在iOS 10+中,这是一个非常简单的两步过程。

  1. 确保你所有的单元格内容都放在一个单独的UIView中(或者像UIStackView那样简化了自动布局的UIView的后代)。 就像dynamic调整UITableViewCells的大小,整个视图层次结构需要configuration约束,从最外层的容器到最内层的视图。 这包括UICollectionViewCell和直接的子视图之间的约束

  2. 指示您的UICollectionView的stream程布局自动resize

     yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayoutAutomaticSize 

经过一段时间的努力,我注意到如果你不禁用滚动,resize不适用于UITextViews:

 let textView = UITextView() textView.scrollEnabled = false 

我做了一个收集视图的dynamic单元格高度。 这里是git hub回购 。

然后,挖掘为什么preferredLayoutAttributesFittingAttributes被多次调用。 其实至less要三次。

控制台日志图片: 在这里输入图像说明

1st preferredLayoutAttributesFittingAttributes

 (lldb) po layoutAttributes <UICollectionViewLayoutAttributes: 0x7fa405c290e0> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (15 12; 384 57.5); (lldb) po self.collectionView <UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0); 

layoutAttributes.frame.size.height是当前状态57.5

2nd preferredLayoutAttributesFittingAttributes

 (lldb) po layoutAttributes <UICollectionViewLayoutAttributes: 0x7fa405c16370> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); (lldb) po self.collectionView <UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 0); 

正如我们预期的那样,单元格框架高度更改为534.5 。 但是,收集视图仍然是零高度。

3rd preferredLayoutAttributesFittingAttributes

 (lldb) po layoutAttributes <UICollectionViewLayoutAttributes: 0x7fa403d516a0> index path: (<NSIndexPath: 0xc000000000000016> {length = 2, path = 0 - 0}); frame = (15 12; 384 534.5); (lldb) po self.collectionView <UICollectionView: 0x7fa40606c800; frame = (0 57.6667; 384 477); 

您可以看到集合视图高度已从0更改为477

行为与处理滚动类似:

 1. Before self-sizing cell 2. Validated self-sizing cell again after other cells recalculated. 3. Did changed self-sizing cell 

一开始,我以为这个方法只能调用一次。 所以我编码如下:

 CGRect frame = layoutAttributes.frame; frame.size.height = frame.size.height + self.collectionView.contentSize.height; UICollectionViewLayoutAttributes* newAttributes = [layoutAttributes copy]; newAttributes.frame = frame; return newAttributes; 

这一行:

 frame.size.height = frame.size.height + self.collectionView.contentSize.height; 

会导致系统调用无限循环和应用程序崩溃。

任何大小的改变,它会一次又一次地validation所有单元格的preferredLayoutAttributesFittingAttributes,直到每个单元格的位置(即框架)都没有更改。

  1. 在viewDidLoad()上添加flowLayout

     override func viewDidLoad() { super.viewDidLoad() if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize(width: 1, height:1) } } 
  2. 另外,为你的单元格设置一个UIView作为mainContainer,并在其中添加所有必需的视图。

  3. 参考这个真棒,令人兴奋的教程进一步参考: UICollectionView与iOS 9和10中使用自动布局自动化单元格

上面的示例方法不能编译。 这是一个更正的版本(但未经testing是否有效)。

 override func preferredLayoutAttributesFittingAttributes(layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { let attr: UICollectionViewLayoutAttributes = layoutAttributes.copy() as! UICollectionViewLayoutAttributes var newFrame = attr.frame self.frame = newFrame self.setNeedsLayout() self.layoutIfNeeded() let desiredHeight: CGFloat = self.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height newFrame.size.height = desiredHeight attr.frame = newFrame return attr } 

任何可能有帮助的人,

如果estimatedItemSize被设置,我有这个讨厌的崩溃。 即使我在numberOfItemsInSection返回0。 因此,单元格本身及其自动布局不是崩溃的原因… collectionView刚刚崩溃,即使是空的,只是因为estimatedItemSize被设置为自定义大小。

在我的情况下,我重新组织我的项目,从一个控制器包含一个collectionView到一个collectionViewController,它的工作。

去搞清楚。

如果你实现UICollectionViewDelegateFlowLayout方法:

- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath

当你调用collectionview performBatchUpdates:completion: ,size的高度将使用sizeForItemAtIndexPath而不是preferredLayoutAttributesFittingAttributes

performBatchUpdates:completion的渲染过程将通过preferredLayoutAttributesFittingAttributes方法,但忽略您的更改。

更新更多信息:

  • 如果你使用flowLayout.estimatedItemSize ,build议使用iOS8.3以后的版本。 在iOS8.3之前,它会崩溃[super layoutAttributesForElementsInRect:rect]; 。 错误消息是

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'

  • 其次,在iOS8.x版本中, flowLayout.estimatedItemSize会导致不同的部分embedded设置不起作用。 即函数: (UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:

对于任何尝试了一切都没有运气的人来说,这是唯一能够为我工作的东西。 对于单元格内的多行标签,可以尝试添加这条神奇的线条:

 label.preferredMaxLayoutWidth = 200 

更多信息: 在这里

干杯!

我试着使用estimatedItemSize但是当插入和删除单元格时,如果estimatedItemSize与单元格的高度不完全相同,会出现一些错误。 我停止设置estimatedItemSize并通过使用原型单元格来实现dynamic单元格。 这是如何完成的:

创build这个协议:

 protocol SizeableCollectionViewCell { func fittedSize(forConstrainedSize size: CGSize)->CGSize } 

在你的自定义UICollectionViewCell实现这个协议:

 class YourCustomCollectionViewCell: UICollectionViewCell, SizeableCollectionViewCell { @IBOutlet private var mTitle: UILabel! @IBOutlet private var mDescription: UILabel! @IBOutlet private var mContentView: UIView! @IBOutlet private var mTitleTopConstraint: NSLayoutConstraint! @IBOutlet private var mDesciptionBottomConstraint: NSLayoutConstraint! func fittedSize(forConstrainedSize size: CGSize)->CGSize { let fittedSize: CGSize! //if height is greatest value, then it's dynamic, so it must be calculated if size.height == CGFLoat.greatestFiniteMagnitude { var height: CGFloat = 0 /*now here's where you want to add all the heights up of your views. apple provides a method called sizeThatFits(size:), but it's not implemented by default; except for some concrete subclasses such as UILabel, UIButton, etc. search to see if the classes you use implement it. here's how it would be used: */ height += mTitle.sizeThatFits(size).height height += mDescription.sizeThatFits(size).height height += mCustomView.sizeThatFits(size).height //you'll have to implement this in your custom view //anything that takes up height in the cell has to be included, including top/bottom margin constraints height += mTitleTopConstraint.constant height += mDescriptionBottomConstraint.constant fittedSize = CGSize(width: size.width, height: height) } //else width is greatest value, if not, you did something wrong else { //do the same thing that's done for height but with width, remember to include leading/trailing margins in calculations } return fittedSize } } 

现在让你的控制器符合UICollectionViewDelegateFlowLayout ,并在其中有这个字段:

 class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout { private var mCustomCellPrototype = UINib(nibName: <name of the nib file for your custom collectionviewcell>, bundle: nil).instantiate(withOwner: nil, options: nil).first as! SizeableCollectionViewCell } 

它将被用作原型单元格来绑定数据,然后确定数据如何影响您想要变为dynamic的维度

最后, UICollectionViewDelegateFlowLayout's collectionView(:layout:sizeForItemAt:)必须被实现:

 class YourViewController: UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { private var mDataSource: [CustomModel] func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath)->CGSize { //bind the prototype cell with the data that corresponds to this index path mCustomCellPrototype.bind(model: mDataSource[indexPath.row]) //this is the same method you would use to reconfigure the cells that you dequeue in collectionView(:cellForItemAt:). i'm calling it bind //define the dimension you want constrained let width = UIScreen.main.bounds.size.width - 20 //the width you want your cells to be let height = CGFloat.greatestFiniteMagnitude //height has the greatest finite magnitude, so in this code, that means it will be dynamic let constrainedSize = CGSize(width: width, height: height) //determine the size the cell will be given this data and return it return mCustomCellPrototype.fittedSize(forConstrainedSize: constrainedSize) } } 

就是这样。 以这种方式返回collectionView(:layout:sizeForItemAt:)中的单元格的大小,使我不必使用estimatedItemSize ,插入和删除单元格完美工作。