Autosizing Cells in UICollectionView (all in code)

Deadly 提交于 2019-12-11 14:59:56

问题


Using Swift-5.0, Xcode-10.2, iOS-12.2,

There is an issue in my code while trying to achieve "autosizing" of cells in a UICollectionView (using UICollectionViewFlowLayout as its layout).

The height of the cell is content-based (and unknown upfront) - therefore I try to get the cell's height to autosize (the "width" I don't care for now and can, for example, be set to frame.width).

Even tough, I only use one large UICollectionView-Cell for the example below, I would still like to keep "dequeueing" of the cells alive since later on, there will be many more cells that need to be filled with large content. Therefore, to make this example more simple, I keept the numberOfItemsInSection at 1.

Moreover, for the below example, each custom CollectionViewCell is filled with a vertical StackView (vertically adding-up a couple of Labels and ImageViews). The StackView is not important here, but I made it as a quick example. Again, the content of the custom CollectionViewCell will change later on (especially it will change to a content-dependent height). The fixed-height cell-content in the code below is just for example reasons...

But what I would still like to get out of this, is the question on how to make the CollectionViewCell autosize its height according to the content (whether fixed-height like in the below example or dynamic content-based-height like in the future implementation) ???

I keep getting the following Constraint-error:

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. 

Everyting works, if I give the Cell a very large fixed height using the method sizeForItemAt inside my UICollectionViewController. (i.e. with "very large" I mean much bigger than the cell's height turns out when dequeueing of the cell takes place).

But as explained above, I do not want to set the cell's height to a fixed value (using the UICollectionViewController's method sizeForItemAt - but rather achieve the desired autosizing.

To autosize the cell, I tried the following:

Approach A)

Use collectionViewFlowLayout.estimatedItemSize = CGSize(...)

(and don't use sizeForItemAt method)

--> Again same thing: If the estimated-size is set large enough, no error occurs. If I set it too small, then I get the same Constraint-error...

Approach B)

Use collectionViewFlowLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize

(and use or don't use sizeForItemAt method - does not make a difference)

--> Again same error: As soon as I start scrolling on the UICollectionView-cell, it throws the same Constraint-error...

Somewhat promising is the observation that using UICollectionViewFlowLayout.automaticSize makes the App work as desired (except that the above error is still thrown - but strange-enough, the App continues somehow to run anyway). For me it is not acceptable that the App works and an error is thrown. The question is, how to get rid of the Constraint-error ??

Here is all the code that I use:

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")
    }   
}

Here is the 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")
    }
}

Needed extensions are defined as such:

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
    }
}

To abstract away the anchoring and height-definitions of the autolayout-constraints of the cell's components (i.e. a bunch of labels and imageViews that fill the cell), I used the following UIView extension...:

(The extension has been published by Brian Voong - see 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:


Finally, after some more digging, I've found a solution:

Four things are important if you want to autosize your custom UICollectionViewCell:

  1. when creating your UICollectionViewController instance, make sure you pass a FlowLayout having set the estimatedItemSize property to some educated-guess value (in my case with a height = 1 [since unknown])
let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.estimatedItemSize = CGSize(width: UIScreen.main.bounds.width, height: 1)

let vc = TestViewController(collectionViewLayout: collectionViewFlowLayout)

.

  1. create a property inside your custom UICollectionViewCell that keeps track of the cell's height

(i.e. inside your UICollectionViewCell class, you will always know what the cell's height is (even if dynamically changed later on). Again, I wanted to get rid of the fact that I needed to define this height already at the UICollectionViewController's sizeForItemAt method before dequeueing the cell)

  1. Don't forget to add the preferredLayoutAttributesFitting method inside your custom UICollectionViewCell :
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
}
  1. Of course, whenever your custom CollectionViewCell changes its height dynamically, you also need to change the myContentHeight property again...

(the calling of preferredLayoutAttributesFitting you don't need to care yourself in code, this method will be called automatically whenever the user enters the view, scrolls or does anything else. The OS takes more or less care of this...).



来源:https://stackoverflow.com/questions/55675885/autosizing-cells-in-uicollectionview-all-in-code

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!