我正在尝试使用自动版式来自动调整大小,但似乎无法使单元格自行调整大小以适应内容。我无法理解如何根据单元的contentView内部内容更新单元的大小。

这里是我尝试过的设置:


自定义UICollectionViewCells,其contentView中带有UICollectionViewCell
滚动UITextView的功能已禁用。
contentView的水平约束为:“ H:| [_textView(320)]”,即,UITextView固定在左侧单元格的显式宽度为320。
contentView的垂直约束为:“ V:| -0-[_ textView]”,即,UITextView固定在单元格的顶部。
UITextView具有高度限制设置为一个常量,UITextView报告将适合该文本。

这是单元格背景设置为红色,而UITextView背景设置为蓝色的情况:


我将一直在玩的项目放在GitHub上。

评论

您要实现sizeForItemAtIndexPath方法吗?

@DanielGalasko我不是。我的理解是,iOS 8中新的自调整大小单元功能不再需要您这样做。

不确定从何处获得该信息,但仍然需要查看WWDC摘录asciiwwdc.com/2014/sessions/226

但这又要取决于您的用例,请确保实现estimatedItemSize

2019年-务必要看一眼:stackoverflow.com/a/58108897/294884

#1 楼

已将Swift 5更新为

preferredLayoutAttributesFittingAttributes重命名为preferredLayoutAttributesFitting并使用自动调整大小


已更新为Swift 4

systemLayoutSizeFittingSize重命名为systemLayoutSizeFitting


针对iOS 9更新了

看到我的GitHub解决方案在iOS 9下出现问题后,我终于有时间充分研究此问题。我现在已经更新了该存储库,以包括几个用于自定义大小单元的不同配置的示例。我的结论是,自我调整大小的单元格在理论上很棒,但在实践中却很混乱。在继续进行自调整单元格设置时要小心。

TL; DR

检查我的GitHub项目


自调整单元格仅支持流布局,因此请确保您所使用的是什么。

需要设置两件事,以使自定义尺寸的单元正常工作。

1。在estimatedItemSize上设置UICollectionViewFlowLayout


一旦设置了estimatedItemSize属性,流布局实际上将变得动态。

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

2。添加对您的单元格子类大小调整的支持

这有2种样式; preferredLayoutAttributesFittingAttributes的自动布局或自定义替代。

使用“自动布局”创建和配置单元格

我不会在此做详细介绍,因为有一篇很棒的SO帖子关于配置单元的约束。只需警惕Xcode 6在iOS 7上破坏了很多东西,因此,如果您支持iOS 7,则需要做一些事情,例如确保在单元格的contentView上设置了autoresizingMask,并且在以下情况下将contentView的边界设置为单元格的边界单元已加载(即awakeFromNib)。

您需要意识到的是,与表格视图单元相比,单元需要受到更严格的约束。例如,如果您希望宽度是动态的,则您的单元格需要高度限制。同样,如果希望高度是动态的,则需要对单元格进行宽度限制。

在自定义单元格中实施preferredLayoutAttributesFittingAttributes

调用此函数时,您的视图已配置了内容(即,已调用cellForItem)。假设已经适当设置了约束,则可以有一个类似以下的实现:

//forces the system to do one layout pass
var isHeightCalculated: Bool = false

override func preferredLayoutAttributesFitting(_ 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上,该行为发生了一些变化,如果您执行此操作,则会导致实现崩溃不小心(在此处查看更多信息)。实施preferredLayoutAttributesFittingAttributes时,需要确保仅更改布局属性的框架一次。如果不这样做,则布局将无限期调用您的实现,并最终崩溃。一种解决方案是将计算出的大小缓存在您的单元格中,并在每次重复使用该单元格或更改其内容时使此值无效,就像我对isHeightCalculated属性所做的那样。

体验布局

此时,您应该在collectionView中具有“运行中”的动态单元格。在测试期间,我还没有发现开箱即用的解决方案就足够了,因此如有需要,请随时发表评论。仍然感觉像UITableView赢得了动态调整IMHO的战斗。

注意事项

请注意,如果您使用原型单元格来计算estimatedItemSize-如果您使用XIB使用尺寸等级。原因是,当您从XIB加载单元时,其大小类将配置为Undefined。这只会在iOS 8及更高版本上被打破,因为在iOS 7上将根据设备加载大小类(iPad =常规-任何,iPhone =紧凑-任何)。您可以在不加载XIB的情况下设置estimatedItemSize,也可以从XIB加载单元,将其添加到collectionView(这将设置traitCollection),执行布局,然后将其从超级视图中删除。或者,您也可以使单元格覆盖traitCollection getter并返回适当的特征。由你决定。

让我知道我是否错过了任何事情,希望对我有帮助并祝您好运



评论


我正在尝试用流布局实现相同的功能,但是当我将它以更新的大小返回newFrame时,布局似乎并没有重新计算y位置,这些位置仍然基于estimateSize中的高度。少了什么东西?

–安娜
2014年10月6日在21:51

啊,发现差异,您将估计高度设置得很高(400),而我却做了最小值(44)。有帮助。但是,除非您一直滚动浏览,否则contentSize似乎仍然无法正确设置,因此仍然不是很有用。顺便说一句,我将UITextView更改为UILabel,具有相同的行为。

–安娜
2014年10月8日14:15

这似乎在iOS 9 GM上从根本上打破了。设置EstimatedItemSize会导致崩溃-在自动布局尝试处理UICollectionViewCell的方式中似乎存在一些巨大的错误。

–Wes Campaigne
2015年9月15日23:45

遇到类似问题,有人找到解决方法了吗?

–托尼
2015年10月10日在6:19

正如别人所说,您的解决方案并不总是有效。选择和取消选择ex后,尝试更改单元格的约束。整个集合再次中断。在ios9上,自动大小调整已损坏!我将恢复使用itemSize委托并自己进行计算。

–JoãoNunes
16-4-5的14:00

#2 楼

在iOS10中,有一个名为UICollectionViewFlowLayout.automaticSize(以前称为UICollectionViewFlowLayoutAutomaticSize)的新常量,因此:

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


您可以使用此常量:

self.flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize


它具有更好的性能,尤其是当您的集合视图中的单元格具有恒定wid时

访问流布局:

override func viewDidLoad() {
   super.viewDidLoad()

   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
   }
}


Swift 5已更新:

override func viewDidLoad() {
   super.viewDidLoad()

   if let flowLayout = collectionView?.collectionViewLayout as? UICollectionViewFlowLayout {
      flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    }
}


评论


您在哪里设置?在viewdidload?您如何处理flowlayout?

– UKDataGeek
17年4月1日在10:31

我设法实现了这一点-但它有一个奇怪的行为,如果有一个单元格,它将使其居中。这是关于它的堆栈溢出问题-我难过吗:stackoverflow.com/questions/43266924/…

– UKDataGeek
17年4月7日在7:38

您可以通过collectionViews collectionViewLayout访问流布局,只需检查其类型是否为UICollectionViewFlowLayout

– d4Rk
18 Mar 19 '18 at 12:08

它使我的细胞很小,没用

–user924
19年4月14日在16:35

#3 楼

丹尼尔·加拉斯科(Daniel Galasko)的答案进行了一些关键更改,解决了我所有的问题。不幸的是,我没有足够的声誉直接发表评论。

在步骤1中,使用“自动版式”时,只需将单个父UIView添加到单元格中。单元格中的所有内容都必须是父级的子视图。那回答了我所有的问题。虽然Xcode会自动为UITableViewCells添加此代码,但不会(但应该)为UICollectionViewCells添加它。根据文档:


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


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

评论


有趣;这是专门针对Xibs的,但还是要感谢,将尝试与Github项目混为一谈,看看我是否可以复制。也许将答案分为Xib vs程序化

–丹尼尔·加拉斯科(Daniel Galasko)
2015年9月2日,下午5:13

很有帮助 。谢谢!

–ProblemSlover
2015年9月7日在17:10

iOS 9,Xcode7。单元在情节提要中进行原型化,设置自定义子类。尝试创建一个名为contentView的属性时,Xcode抱怨它与现有属性冲突。尝试将子视图添加到self.contentView并为其设置约束,然后应用崩溃。

–尼古拉斯·米亚里(Nicolas Miari)
2015年10月6日,11:06

@NicolasMiari我不知道如何以编程方式执行此操作,我的解决方案实际上只是在XIB中添加一个uiview,所有内容都放在其中。您无需创建属性或其他任何内容。

–马特考拉
2015年10月7日18:56

我面临与@NicolasMiari相同的问题。当我为在iOS 9 / Xcode 7上具有自动布局约束的情节提要中原型化的单元格设置估算的ContentSize时,应用程序崩溃,其中包含错误的访问异常并且没有有用的堆栈跟踪

–山葵
15年12月6日在17:22

#4 楼

在iOS 10以上版本中,这是一个非常简单的两步过程。


确保将所有单元格内容放置在单个UIView中(或放置在UIView的后代中,例如UIStackView,这样可以简化自动布局很多)。就像动态调整UITableViewCells的大小一样,从最外面的容器到最里面的视图,整个视图层次结构都需要配置约束。其中包括UICollectionViewCell和直接子视图之间的约束

指示UICollectionView的布局自动调整大小

yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize




评论


我很好奇,如何将单元格内容包装在UIView中使自动版式的性能更好?我以为它会在层次结构更浅的情况下更好地执行?

–詹姆斯·希尔德(James Heald)
17年12月1日在2:29

我没有提到性能,只是简单。仅当您的约束在左右之间以及上下之间一直存在时,才可以使用自调整大小的单元格。当所有视图都包装在一个容器中时,最容易实现。如果该容器是UIStackView,则它比UIView的任何其他子类都容易。

– Marmoy
17年1月1日在8:40



您能否告诉我如何为UICollectionViewFlowLayoutAutomaticSize设置高度常数(例如将高度设置为25,宽度将自动更改)。

– Svetoslav Atanasov
19年1月25日在14:58

使用Swift 4.1,Xcode告诉我UICollectionViewFlowLayout.automaticSize已重命名为UICollectionViewFlowLayoutAutomaticSize。

– LinusGeffarth
19年2月22日在17:03



#5 楼



在viewDidLoad()上添加flowLayout

override func viewDidLoad() {
    super.viewDidLoad() 
    if let flowLayout = infoCollection.collectionViewLayout as? UICollectionViewFlowLayout {
        flowLayout.estimatedItemSize = CGSize(width: 1, height:1)
    }
}


此外,将UIView设置为单元格的mainContainer并在其中添加所有必需的视图。 br />请参阅此功能强大,令人赞叹的教程以获取更多参考:
UICollectionView具有在iOS 9和10中使用自动布局功能自动调整单元格的大小


#6 楼

编辑11/19/19:对于iOS 13,只需使用具有估计高度的UICollectionViewCompositionalLayout。不要浪费您的时间来处理这个损坏的API。

经过一段时间的努力,我注意到如果不禁用滚动,则调整大小对UITextViews不起作用:

let textView = UITextView()
textView.scrollEnabled = false


评论


当我尝试滚动时,集合视图单元格不平滑。您对此有什么解决方案吗?

– Sathish Kumar Gurunathan
18年8月28日在9:54



#7 楼

contentView锚的奥秘:
在一个奇怪的情况下,这
    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)
        ])
    }
}

和往常一样
YourLayout: UICollectionViewFlowLayout
在q4312079q
谁知道?可能会帮助某人。
信贷
https://www.vadimbulavin.com/collection-view-cells-self-sizing/
偶然发现那里的小费-从未在其他地方看到过与此相关的所有1000篇文章。

#8 楼

我做了一个集合视图的动态单元格高度。这是git hub仓库。

然后,找出为什么多次调用preferredLayoutAttributesFittingAttributes的原因。实际上,它将至少被调用3次。

控制台日志图片:


第一个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。

第二个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,与我们的预期相同。但是,集合视图的高度仍为零。

第三个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;


将导致系统调用无限循环和应用崩溃。

任何大小的更改,它将一次又一次地验证所有单元格的preferredLayoutAttributesFittingAttributes,直到每个单元格的位置(即帧)不再变化为止。

#9 楼

除了上述答案,

请确保将UICollectionViewFlowLayout的EstimatedItemSize属性设置为某个大小,并且不要实现sizeForItem:atIndexPath委托方法。就是这样。
>

#10 楼

该解决方案包括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
    }

    ...
}


由于要启用UITextView的自定义大小,因此还有一个附加步骤;
3。计算并设置UITextView的heightAnchor.constant。
因此,每当设置contentView的宽度时,我们都会在didSetmaxWidth中调整UITextView的高度。
在UICollectionViewCell内部:
class MultiLineCell: UICollectionViewCell{
    ....

    var maxWidth: CGFloat? {
        didSet {
            guard let maxWidth = maxWidth else {
                return
            }
            containerViewWidthAnchor.constant = maxWidth
            containerViewWidthAnchor.isActive = true
        }
    }

    ....
}

/>这些步骤将为您带来所需的结果。
完整的可运行要领
参考:Vadim Bulavin博客文章-集合视图单元格的自调整大小:分步教程
截屏:


评论


很棒的答案👏

–迈克尔
9月16日下午2:44

#11 楼

如果实现UICollectionViewDelegateFlowLayout方法:调用- (CGSize)collectionView:(UICollectionView*)collectionView layout:(UICollectionViewLayout*)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath*)indexPath 时,大小高度将使用collectionview performBatchUpdates:completion:而不是
sizeForItemAtIndexPath

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

评论


我已经看到了这种不当行为,但仅在估计大小为零时才看到。您确定要设置估算尺寸吗?

–加特伍德
17年2月21日在21:32

#12 楼

对于可能对谁有帮助的人,

如果设置了estimatedItemSize,我会遇到令人讨厌的崩溃。即使我在numberOfItemsInSection中返回了0。因此,单元格本身及其自动布局并不是崩溃的原因... collectionView只是崩溃了,即使是空的,也只是因为estimatedItemSize已设置为自动调整大小。

我将项目从包含collectionView的控制器重组为collectionViewController,并成功了。

去看图。

评论


尝试在collectionView.reloadData()之后调用collectionView.collectionViewLayout.invalidateLayout()。

–chengsam
17年6月27日在2:47

对于ios10及以下版本,请添加此代码`覆盖func viewWillLayoutSubviews(){super.viewWillLayoutSubviews()如果#available(iOS 11.0,*){}否则{mainCollectionView.collectionViewLayout.invalidateLayout()}}`

–马罗斯·乌玛(Marosdee Uma)
18-3-29在8:33



#13 楼

对于那些尝试一切都没有运气的人来说,这是让它为我工作的唯一方法。
对于单元格中的多行标签,请尝试添加以下魔术线:

label.preferredMaxLayoutWidth = 200


更多信息:在此处

干杯!
/>

#14 楼

上面的示例方法无法编译。这是更正的版本(但未经测试是否有效。)

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
}


#15 楼

更新更多信息:



如果使用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:


#16 楼

我尝试使用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,并且插入和删除单元格效果很好。