UICollectionView具有自动布局的自定义单元格
我正在尝试自动调整UICollectionViewCells与自动布局的工作,但我似乎无法让单元格的大小自己的内容。 我很难理解单元格的大小是如何从单元格内容的内容更新的。
这是我试过的设置:
- 在contentView中使用UITextView自定义UICollectionViewCell。
- 滚动的UITextView被禁用。
- contentView的水平约束是:“H:| [_textView(320)]”,即UITextView固定在单元格的左侧,宽度为320。
- contentView的垂直约束是:“V:| -0 – [_ textView]”,即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+中,这是一个非常简单的两步过程。
-
确保你所有的单元格内容都放在一个单独的UIView中(或者像UIStackView那样简化了自动布局的UIView的后代)。 就像dynamic调整UITableViewCells的大小,整个视图层次结构需要configuration约束,从最外层的容器到最内层的视图。 这包括UICollectionViewCell和直接的子视图之间的约束
-
指示您的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,直到每个单元格的位置(即框架)都没有更改。
-
在viewDidLoad()上添加flowLayout
override func viewDidLoad() { super.viewDidLoad() if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout { flowLayout.estimatedItemSize = CGSize(width: 1, height:1) } }
-
另外,为你的单元格设置一个UIView作为mainContainer,并在其中添加所有必需的视图。
-
参考这个真棒,令人兴奋的教程进一步参考: 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
,插入和删除单元格完美工作。