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

时间:2014-09-17 16:02:16

标签: ios uicollectionview autolayout uicollectionviewcell

我正在尝试自我调整UICollectionViewCells使用自动布局,但我似乎无法让细胞根据内容调整自己的大小。我无法理解如何根据单元格内容视图中的内容更新单元格的大小。

这是我尝试过的设置:

  • 自定义UICollectionViewCell,其contentView为<{1}}。
  • 已停用UITextView的滚动。
  • contentView的水平约束是:“H:| [_textView(320)]”,即UITextView固定在单元格的左侧,显式宽度为320.
  • contentView的垂直约束是:“V:| -0 - [_ textView]”,即固定在单元格顶部的UITextView
  • UITextView将高度约束设置为常量,UITextView报告将适合文本。

以下是单元格背景设置为红色,UITextView背景设置为蓝色的情况: cell background red, UITextView background blue

我把我一直在玩的项目放在GitHub上here

16 个答案:

答案 0 :(得分:261)

更新了Swift 4

systemLayoutSizeFittingSize已重命名为systemLayoutSizeFitting


针对iOS 9

进行了更新

在iOS 9下看到我的GitHub解决方案中断后,我终于有时间充分调查此问题。我现在更新了repo,以包含自我调整单元格的不同配置的几个示例。我的结论是自我定型细胞在理论上很好,但在实践中却很混乱。在进行自我调整细胞时要谨慎。

<强> TL; DR

查看我的GitHub project


自定义单元格仅支持流程布局,因此请确保您正在使用的内容。

您需要设置两件事来使自定义单元格起作用。

1。在estimatedItemSize

上设置UICollectionViewFlowLayout

设置estimatedItemSize属性后,流布局将变为动态。

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

2。添加对细胞子类

的大小调整的支持

这有两种口味;自动布局自定义覆盖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+中,这是一个非常简单的两步过程。

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

  2. 指示您的UICollectionView的流程布局自动调整大小

    yourFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
    

答案 4 :(得分:11)

  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 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次。

控制台日志图片: enter image description here

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个简单步骤:

  1. 启用动态单元大小调整

flowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

  1. 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的自定义大小,因此它还有一个附加步骤;

3。计算并设置UITextView的heightAnchor.constant。

因此,无论何时设置contentView的宽度,我们都会在didSet的{​​{1}}中调整UITextView的高度。

内部UICollectionViewCell:

maxWidth

这些步骤将为您带来所需的结果。

Complete runnable gist

参考:Vadim Bulavin博客文章-Collection View Cells Self-Sizing: Step by Step Tutorial

屏幕截图:

enter image description here

答案 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锚点之谜:

在一个奇怪的情况下,这

    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,并且插入和删除单元格完美无缺。