我正在尝试自我调整UICollectionViewCells
使用自动布局,但我似乎无法让细胞根据内容调整自己的大小。我无法理解如何根据单元格内容视图中的内容更新单元格的大小。
这是我尝试过的设置:
UICollectionViewCell
,其contentView为<{1}}。UITextView
的滚动。UITextView
固定在单元格的左侧,显式宽度为320. UITextView
。UITextView
将高度约束设置为常量,UITextView
报告将适合文本。以下是单元格背景设置为红色,UITextView
背景设置为蓝色的情况:
我把我一直在玩的项目放在GitHub上here。
答案 0 :(得分:261)
systemLayoutSizeFittingSize
已重命名为systemLayoutSizeFitting
在iOS 9下看到我的GitHub解决方案中断后,我终于有时间充分调查此问题。我现在更新了repo,以包含自我调整单元格的不同配置的几个示例。我的结论是自我定型细胞在理论上很好,但在实践中却很混乱。在进行自我调整细胞时要谨慎。
<强> TL; DR 强>
查看我的GitHub project
自定义单元格仅支持流程布局,因此请确保您正在使用的内容。
您需要设置两件事来使自定义单元格起作用。
estimatedItemSize
UICollectionViewFlowLayout
设置estimatedItemSize
属性后,流布局将变为动态。
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
这有两种口味;自动布局或自定义覆盖preferredLayoutAttributesFittingAttributes
。
我不会详细介绍这一点,因为有关于为单元格配置约束的精彩SO post。只要小心Xcode 6 broke iOS 7中的一堆内容,如果你支持iOS 7,你需要做的事情就像确保在单元格的contentView上设置autoresizingMask并且将contentView的边界设置为单元格的加载单元格时的边界(即awakeFromNib
)。
您需要注意的事情是,您的单元格需要比表格单元格更严格地受到约束。例如,如果您希望宽度是动态的,那么您的单元格需要高度约束。同样,如果您希望高度是动态的,那么您需要对细胞进行宽度约束。
preferredLayoutAttributesFittingAttributes
调用此函数时,您的视图已配置了内容(即已调用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.systemLayoutSizeFitting(layoutAttributes.size)
var newFrame = layoutAttributes.frame
newFrame.size.width = CGFloat(ceilf(Float(size.width)))
layoutAttributes.frame = newFrame
isHeightCalculated = true
}
return layoutAttributes
}
注意在iOS 9上,如果您不小心,行为会发生一些变化,可能会导致您的实施崩溃(请参阅更多here)。实施preferredLayoutAttributesFittingAttributes
时,您需要确保只更改一次布局属性的框架。如果您不这样做,布局将无限期地调用您的实现并最终崩溃。一种解决方案是在单元格中缓存计算出的大小,并在重复使用单元格时使其无效,或者像使用isHeightCalculated
属性一样更改其内容。
此时,您应该在collectionView中拥有“正常运行”的动态单元格。我在测试期间还没有找到足够的开箱即用解决方案,所以如果有的话,请随时发表评论。它仍然感觉像UITableView
赢得了动态调整恕我直言的战斗。
请注意,如果您使用原型单元格来计算estimatedItemSize - 如果您的XIB uses size classes,这将会中断。原因是当您从XIB加载单元格时,其大小类将配置为Undefined
。这只会在iOS 8及更高版本上打破,因为在iOS 7上将根据设备加载大小类(iPad = Regular-Any,iPhone = Compact-Any)。您可以在不加载XIB的情况下设置estimatedItemSize,也可以从XIB加载单元格,将其添加到collectionView(这将设置traitCollection),执行布局,然后将其从superview中删除。或者,您也可以让您的单元格覆盖traitCollection
getter并返回相应的特征。由你决定。
如果我错过任何内容,请告诉我,希望我帮助并祝你好运编码
答案 1 :(得分:40)
Daniel Galasko's answer的一些关键更改修复了我的所有问题。不幸的是,我没有足够的声誉直接评论(还)。
在步骤1中,使用自动布局时,只需将单个父UIView添加到单元格即可。单元格内的所有内容都必须是父级的子视图。这回答了我所有的问题。虽然Xcode自动为UITableViewCells添加了这个,但它对UICollectionViewCells来说并不是(但应该)。根据{{3}}:
要配置单元格的外观,请将显示数据项内容所需的视图添加为contentView属性中视图的子视图。不要直接将子视图添加到单元格本身。
然后完全跳过第3步。这不是必需的。
答案 2 :(得分:30)
在iOS10中有一个名为UICollectionViewFlowLayout.automaticSize
的新常量(以前为UICollectionViewFlowLayoutAutomaticSize
),所以相反:
self.flowLayout.estimatedItemSize = CGSize(width: 100, height: 100)
你可以用这个:
self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
它具有更好的性能,尤其是当您的集合视图中的单元格具有常量wid
时答案 3 :(得分:28)
在iOS 10+中,这是一个非常简单的两步过程。
确保所有单元格内容都放在单个UIView中(或者在UIView的后代内部,如UIStackView,这简化了自动布局)。就像动态调整UITableViewCells一样,整个视图层次结构需要配置约束,从最外层容器到最内层视图。这包括UICollectionViewCell和直接子视图之间的约束
指示您的UICollectionView的流程布局自动调整大小
yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
答案 4 :(得分:11)
在viewDidLoad()
上添加flowLayoutoverride func viewDidLoad() {
super.viewDidLoad()
if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
}
}
此外,将UIView设置为您的单元格的mainContainer,并在其中添加所有必需的视图。
请参阅这个令人敬畏的,令人兴奋的教程以获得进一步的参考: UICollectionView with autosizing cell using autolayout in iOS 9 & 10
答案 5 :(得分:5)
经过一段时间的努力,我注意到如果不禁用滚动,调整大小对UITextViews不起作用:
let textView = UITextView()
textView.scrollEnabled = false
答案 6 :(得分:2)
我做了一个动态单元格高度的集合视图。这是git hub repo。
并且,挖掘出为什么不止一次调用preferredLayoutAttributesFittingAttributes。实际上,它至少会被调用3次。
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;
会导致系统调用无限循环和App崩溃。
任何尺寸改变,它将验证所有细胞&#39; preferredLayoutAttributesFittingAttributes一次又一次,直到每个单元格&#39;位置(即框架)不再发生变化。
答案 7 :(得分:2)
该解决方案包括3个简单步骤:
flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
collectionView(:cellForItemAt:)
设置containerView.widthAnchor.constraint以将contentView的宽度限制为collectionView的宽度。class ViewController: UIViewController, UICollectionViewDataSource {
...
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cellId", for: indexPath) as! MultiLineCell
cell.textView.text = dummyTextMessages[indexPath.row]
cell.maxWidth = collectionView.frame.width
return cell
}
...
}
class MultiLineCell: UICollectionViewCell{
....
var maxWidth: CGFloat? {
didSet {
guard let maxWidth = maxWidth else {
return
}
containerViewWidthAnchor.constant = maxWidth
containerViewWidthAnchor.isActive = true
}
}
....
}
由于您要启用UITextView的自定义大小,因此它还有一个附加步骤;
因此,无论何时设置contentView的宽度,我们都会在didSet
的{{1}}中调整UITextView的高度。
内部UICollectionViewCell:
maxWidth
这些步骤将为您带来所需的结果。
参考:Vadim Bulavin博客文章-Collection View Cells Self-Sizing: Step by Step Tutorial
屏幕截图:
答案 8 :(得分:1)
如果实现UICollectionViewDelegateFlowLayout方法:
- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath
当您致电collectionview performBatchUpdates:completion:
时,尺寸高度将使用sizeForItemAtIndexPath
代替
preferredLayoutAttributesFittingAttributes
。
performBatchUpdates:completion
的呈现过程将通过方法preferredLayoutAttributesFittingAttributes
,但忽略了您的更改。
答案 9 :(得分:1)
对任何人都有帮助,
如果设置了estimatedItemSize
,我就会遇到令人讨厌的崩溃。即使我在numberOfItemsInSection
中返回0。因此,单元格本身及其自动布局不是崩溃的原因......即使是空的,collectionView也只是崩溃了,只是因为estimatedItemSize
被设置为自我调整大小。
在我的情况下,我重新组织了我的项目,从包含collectionView的控制器到collectionViewController,它工作正常。
去图。
答案 10 :(得分:1)
在一个奇怪的情况下,这
contentView.translatesAutoresizingMaskIntoConstraints = false
不起作用。在contentView中添加了四个显式锚点,并且可以正常工作。
class AnnoyingCell: UICollectionViewCell {
@IBOutlet var word: UILabel!
override init(frame: CGRect) {
super.init(frame: frame); common() }
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder); common() }
private func common() {
contentView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
contentView.leftAnchor.constraint(equalTo: leftAnchor),
contentView.rightAnchor.constraint(equalTo: rightAnchor),
contentView.topAnchor.constraint(equalTo: topAnchor),
contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
}
}
和往常一样
estimatedItemSize = UICollectionViewFlowLayout.automaticSize
在YourLayout: UICollectionViewFlowLayout
谁知道?可能会帮助某人。
https://www.vadimbulavin.com/collection-view-cells-self-sizing/
偶然发现那里的尖端-在所有1000篇文章中再也没有看到它。
答案 11 :(得分:0)
上面的示例方法无法编译。这是一个更正版本(但未经测试是否有效。)
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
}
答案 12 :(得分:0)
更新更多信息:
如果您使用flowLayout.estimatedItemSize
,建议使用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
会导致不同的部分插入设置无法正常工作。即功能:(UIEdgeInsets)collectionView:layout:insetForSectionAtIndex:
。
答案 13 :(得分:0)
除了上述答案,
只需确保将 UICollectionViewFlowLayout 的 estimatedItemSize 属性设置为某个大小,并且不要实施 sizeForItem:atIndexPath 委托方法。
就是这样。
答案 14 :(得分:-1)
对于那些没有运气尝试过一切的人来说,这是唯一让它为我工作的东西。 对于单元格内的多行标签,请尝试添加此魔术行:
label.preferredMaxLayoutWidth = 200
更多信息:here
干杯!
答案 15 :(得分:-2)
我尝试使用estimatedItemSize
但是如果estimatedItemSize
不完全等于单元格的高度,则在插入和删除单元格时会出现一堆错误。我停止设置estimatedItemSize
并使用原型单元格实现动态单元格。这是怎么做的:
创建此协议:
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
}
它将用作原型单元格来绑定数据,然后确定该数据如何影响您想要动态的维度
最后,必须实施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
,并且插入和删除单元格完美无缺。