在 UICollectionView 中自动调整单元格(全部在代码中)

Posted

技术标签:

【中文标题】在 UICollectionView 中自动调整单元格(全部在代码中)【英文标题】:Autosizing Cells in UICollectionView (all in code) 【发布时间】:2019-04-14 13:37:20 【问题描述】:

使用 Swift-5.0、Xcode-10.2、ios-12.2,

在尝试实现 UICollectionView 中单元格的“自动调整大小”(使用 UICollectionViewFlowLayout 作为其布局)时,我的代码中存在问题。

单元格的高度是基于内容的(并且预先未知) - 因此我尝试让 单元格的高度自动调整大小(我现在不关心的“宽度”可以,例如,设置为frame.width)。

即使很难,我在下面的例子中只使用了一个大的 UICollectionView-Cell,我仍然想保持单元格的“出队”,因为以后会有更多的单元格需要填充大量内容.因此,为了使这个示例更简单,我将numberOfItemsInSection 保留为 1。

此外,对于下面的示例,每个自定义 CollectionViewCell 都填充有一个垂直 StackView(垂直添加了几个 Labels 和 ImageViews)。 StackView 在这里并不重要,但我把它作为一个简单的例子。同样,自定义 CollectionViewCell 的内容稍后会更改(特别是它将更改为依赖于内容的高度)。下面代码中固定高度的单元格内容只是示例原因...

但我仍然想摆脱这个问题,是关于如何使 CollectionViewCell 根据内容自动调整其高度的问题(无论是像下面的示例中的固定高度还是像在未来的实施)???

我不断收到以下约束错误:

Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one
you don't want. 
Try this: 
  (1) look at each constraint and try to figure out which you don't expect; 
  (2) find the code that added the unwanted constraint or constraints and fix it. 

如果我在我的UICollectionViewController 中使用sizeForItemAt 方法给单元格一个非常大的固定高度,那么一切都有效。 (即“非常大”我的意思是比单元格出列时单元格的高度大得多)。

但如上所述,我不想将单元格的高度设置为固定值(使用 UICollectionViewController 的方法 sizeForItemAt - 而是实现所需的自动调整大小。

要自动调整单元格大小,我尝试了以下方法:

方法 A)

使用collectionViewFlowLayout.estimatedItemSize = CGSize(...)

(并且不要使用sizeForItemAt 方法)

--> 同样的事情:如果估计的大小设置得足够大,则不会发生错误。如果我将它设置得太小,那么我会得到相同的约束错误...

方法 B)

使用collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

(使用或不使用sizeForItemAt 方法 - 没有区别)

--> 同样的错误:一旦我开始在 UICollectionView-cell 上滚动,它就会抛出相同的 Constraint-error...

观察到使用UICollectionViewFlowLayout.automaticSize 使应用程序按预期工作(除了上述错误仍然被抛出 - 但很奇怪,应用程序仍然以某种方式继续运行)这一观察结果颇有希望。对我来说,应用程序正常工作并引发错误是不可接受的。问题是,如何摆脱约束错误??

这是我使用的所有代码:

class TestViewController: UICollectionViewController, UICollectionViewDelegateFlowLayout 

    let cellId = "cellID"

    override func viewDidLoad() 
        super.viewDidLoad()
        collectionView.backgroundColor = .yellow
        collectionView.register(MyCollectionViewCell.self, forCellWithReuseIdentifier: cellId)
    

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return 1
    

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! MyCollectionViewCell
        return cell
    

//    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 
//        return .init(width: view.frame.width, height: 4000)
//    

    init() 
        let collectionViewFlowLayout = UICollectionViewFlowLayout()
//        collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 4000)
        collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
        super.init(collectionViewLayout: collectionViewFlowLayout)
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
       

这里是 CustomCollectionViewCell:

import UIKit

class MyCollectionViewCell: UICollectionViewCell 

    let titleLabel1 = UILabel(text: "Title 123", font: .boldSystemFont(ofSize: 30))
    let titleLabel2 = UILabel(text: "Testing...", font: .boldSystemFont(ofSize: 15))
    let imgView1 = UIImageView(image: nil)
    let imgView2 = UIImageView(image: nil)
    let imgView3 = UIImageView(image: nil)
    let imgView4 = UIImageView(image: nil)
    let imgView5 = UIImageView(image: nil)


    let imageView: UIImageView = 
        let imgView = UIImageView()

        imgView.backgroundColor = .green
        return imgView
    ()

    override init(frame: CGRect) 
        super.init(frame: frame)

        backgroundColor = .yellow
        titleLabel1.constrainHeight(constant: 50)
        titleLabel1.constrainWidth(constant: 250)
        titleLabel2.constrainHeight(constant: 50)
        titleLabel2.constrainWidth(constant: 250)

        imgView1.constrainHeight(constant: 100)
        imgView1.constrainWidth(constant: 200)
        imgView1.backgroundColor = .green
        imgView2.constrainHeight(constant: 100)
        imgView2.constrainWidth(constant: 200)
        imgView2.backgroundColor = .green
        imgView3.constrainHeight(constant: 100)
        imgView3.constrainWidth(constant: 200)
        imgView3.backgroundColor = .green
        imgView4.constrainHeight(constant: 100)
        imgView4.constrainWidth(constant: 200)
        imgView4.backgroundColor = .green
        imgView5.constrainHeight(constant: 100)
        imgView5.constrainWidth(constant: 200)
        imgView5.backgroundColor = .green

        let stackView = UIStackView(arrangedSubviews: [titleLabel1, imgView1, titleLabel2, imgView2, imgView3, imgView4, imgView5])

        addSubview(stackView)
        stackView.anchor(top: safeAreaLayoutGuide.topAnchor, leading: leadingAnchor, bottom: bottomAnchor, trailing: trailingAnchor)
        stackView.spacing = 20
        stackView.axis = .vertical
        stackView.alignment = .center
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

需要的扩展定义如下:

extension UILabel 
    convenience init(text: String, font: UIFont) 
        self.init(frame: .zero)
        self.text = text
        self.font = font
        self.backgroundColor = .red
    


extension UIImageView 
    convenience init(cornerRadius: CGFloat) 
        self.init(image: nil)
        self.layer.cornerRadius = cornerRadius
        self.clipsToBounds = true
        self.contentMode = .scaleAspectFill
    

为了抽象出单元格组件的自动布局约束的锚定和高度定义(即填充单元格的一堆标签和图像视图),我使用了以下 UIView 扩展...:

(扩展程序已由 Brian Voong 发布 - 请参阅 video link)

// Reference Video: https://youtu.be/iqpAP7s3b-8
extension UIView 

    @discardableResult
    func anchor(top: NSLayoutYAxisAnchor?, leading: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, trailing: NSLayoutXAxisAnchor?, padding: UIEdgeInsets = .zero, size: CGSize = .zero) -> AnchoredConstraints 

        translatesAutoresizingMaskIntoConstraints = false
        var anchoredConstraints = AnchoredConstraints()

        if let top = top 
            anchoredConstraints.top = topAnchor.constraint(equalTo: top, constant: padding.top)
        

        if let leading = leading 
            anchoredConstraints.leading = leadingAnchor.constraint(equalTo: leading, constant: padding.left)
        

        if let bottom = bottom 
            anchoredConstraints.bottom = bottomAnchor.constraint(equalTo: bottom, constant: -padding.bottom)
        

        if let trailing = trailing 
            anchoredConstraints.trailing = trailingAnchor.constraint(equalTo: trailing, constant: -padding.right)
        

        if size.width != 0 
            anchoredConstraints.width = widthAnchor.constraint(equalToConstant: size.width)
        

        if size.height != 0 
            anchoredConstraints.height = heightAnchor.constraint(equalToConstant: size.height)
        

        [anchoredConstraints.top, anchoredConstraints.leading, anchoredConstraints.bottom, anchoredConstraints.trailing, anchoredConstraints.width, anchoredConstraints.height].forEach $0?.isActive = true 

        return anchoredConstraints
    

    func fillSuperview(padding: UIEdgeInsets = .zero) 
        translatesAutoresizingMaskIntoConstraints = false
        if let superviewTopAnchor = superview?.topAnchor 
            topAnchor.constraint(equalTo: superviewTopAnchor, constant: padding.top).isActive = true
        

        if let superviewBottomAnchor = superview?.bottomAnchor 
            bottomAnchor.constraint(equalTo: superviewBottomAnchor, constant: -padding.bottom).isActive = true
        

        if let superviewLeadingAnchor = superview?.leadingAnchor 
            leadingAnchor.constraint(equalTo: superviewLeadingAnchor, constant: padding.left).isActive = true
        

        if let superviewTrailingAnchor = superview?.trailingAnchor 
            trailingAnchor.constraint(equalTo: superviewTrailingAnchor, constant: -padding.right).isActive = true
        
    

    func centerInSuperview(size: CGSize = .zero) 
        translatesAutoresizingMaskIntoConstraints = false
        if let superviewCenterXAnchor = superview?.centerXAnchor 
            centerXAnchor.constraint(equalTo: superviewCenterXAnchor).isActive = true
        

        if let superviewCenterYAnchor = superview?.centerYAnchor 
            centerYAnchor.constraint(equalTo: superviewCenterYAnchor).isActive = true
        

        if size.width != 0 
            widthAnchor.constraint(equalToConstant: size.width).isActive = true
        

        if size.height != 0 
            heightAnchor.constraint(equalToConstant: size.height).isActive = true
        
    

    func centerXInSuperview() 
        translatesAutoresizingMaskIntoConstraints = false
        if let superViewCenterXAnchor = superview?.centerXAnchor 
            centerXAnchor.constraint(equalTo: superViewCenterXAnchor).isActive = true
        
    

    func centerYInSuperview() 
        translatesAutoresizingMaskIntoConstraints = false
        if let centerY = superview?.centerYAnchor 
            centerYAnchor.constraint(equalTo: centerY).isActive = true
        
    

    func constrainWidth(constant: CGFloat) 
        translatesAutoresizingMaskIntoConstraints = false
        widthAnchor.constraint(equalToConstant: constant).isActive = true
    

    func constrainHeight(constant: CGFloat) 
        translatesAutoresizingMaskIntoConstraints = false
        heightAnchor.constraint(equalToConstant: constant).isActive = true
    

【问题讨论】:

【参考方案1】:

最后,经过一番挖掘,我找到了解决方案:

如果要自动调整自定义 UICollectionViewCell 的大小,有四件事很重要:

    在创建您的 UICollectionViewController 实例时,请确保您传递了一个 FlowLayout 已将 estimatedItemSize 属性设置为一些有根据的猜测值(在我的例子中,高度 = 1 [因为未知])
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 1)

let vc = TestViewController(collectionViewLayout: collectionViewFlowLayout)

.

    在您的自定义 UICollectionViewCell 中创建一个属性来跟踪单元格的高度

(即在您的 UICollectionViewCell 类中,您将始终知道单元格的高度是多少(即使稍后动态更改)。再次,我想摆脱我需要在 UICollectionViewController 的 @ 处定义此高度的事实987654323@出列单元格之前的方法)

    不要忘记在您的自定义 UICollectionViewCell 中添加 preferredLayoutAttributesFitting 方法:
let myContentHeight = CGFloat(720)
// in my above code-example, the 720 are the sum of 2 x 50 (labels) plus 5 x 100 (imageViews) plus 6 x 20 (spacing)

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes 
    setNeedsLayout()
    layoutIfNeeded()
    var newFrame = layoutAttributes.frame
    // myContentHeight corresponds to the total height of your custom UICollectionViewCell
    newFrame.size.height = ceil(myContentHeight)
    layoutAttributes.frame = newFrame
    return layoutAttributes

    当然,每当您的自定义 CollectionViewCell 动态改变其高度时,您还需要再次更改 myContentHeight 属性...

(preferredLayoutAttributesFitting的调用你不需要在代码中关心自己,这个方法会在用户进入视图、滚动或做其他任何事情时自动调用。操作系统或多或少地关心这一点。 ..)。

【讨论】:

在第 4 点,您写来更改 myContentHeight,但是在哪里?以及何时需要更新?

以上是关于在 UICollectionView 中自动调整单元格(全部在代码中)的主要内容,如果未能解决你的问题,请参考以下文章

自动调整UICollectionView高度以调整其内容大小

设备旋转时如何自动调整 UICollectionView 的大小?

自动调整嵌入在 UITableView 中的 UICollectionView 单元格不起作用

我在自定义 TableViewCell 中有一个 UICollectionView,但我无法自动调整 CollectionView 的大小

UICollectionView 具有自动调整单元格(estimatedSize)和 sectionHeadersPinToVisibleBounds 的精神

如何在 UICollectionView 中动态调整标题视图的大小?