UICollectionView with autosizing cell (estimatedSize) and sectionHeadersPinToVisibleBounds goes mental

i0S Swift Issue

Question or problem in the Swift programming language:

Consider the following situation. I have an UICollectionView (inside UICollectionViewController), which looks almost the same as UITableView (the reason why I don’t use UITalbeView is because I have non data views on layout, that I don’t want to manage and mess with my IndexPath).
In order to achieve the autosizing cells I’ve set estimatedItemSize, something like that:

layout.estimatedItemSize = CGSize(width: self.view.bounds.size.width, height: 72)

Also, in my cell I have layout attributes:

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

So, by doing that I’ve got exact layout as UITableView with autosizing. And it works perfectly.

Now, I am trying to add the header and pin it on scrolling to the top of the section, like that:

layout.sectionHeadersPinToVisibleBounds = false

but layout goes into weird state, I have glitches all over the place, cells overlapping each other, and headers sometimes doesn’t stick.

UPDATE:

The code of view controller and cell:

class ViewController: UICollectionViewController {

override func viewDidLoad() {
    super.viewDidLoad()
    let layout = collectionView?.collectionViewLayout as! UICollectionViewFlowLayout
    layout.sectionHeadersPinToVisibleBounds = true
    layout.estimatedItemSize = CGSize(width: collectionView?.bounds.size.width ?? 0, height: 36) // enables dynamic height
}

override func numberOfSections(in collectionView: UICollectionView) -> Int {
    return 10
}

override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return 10
}

override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

    let cell =  collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CustomCell
    cell.heightConstraint.constant = CGFloat(indexPath.row * 10 % 100) + 10 // Random constraint to make dynamic height work

    return cell
}

override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    return collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionElementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath)
}

class CustomCell : UICollectionViewCell {
let identifier = "CustomCell"

@IBOutlet weak var rectangle: UIView!
@IBOutlet weak var heightConstraint: NSLayoutConstraint!

override func awakeFromNib() {
    translatesAutoresizingMaskIntoConstraints = false
}

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    layoutAttributes.bounds.size.height = systemLayoutSizeFitting(UILayoutFittingCompressedSize).height
    return layoutAttributes
}

Details of lagging in video: https://vimeo.com/203284395

How to solve the problem:

Solution 1:

Update from WWDC 2017:

My colleague was on WWDC 2017, and he asked one of the UIKit engineers about this issue. The engineer confirmed that this issue is known bug by Apple and there is no fix at that moment.

Solution 2:

Use the UICollectionViewDelegateFlowLayout method.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize{
        //Calculate your Dynamic Size Here for Each Cell at SpecificIndexPath.
        //For Example You want your Cell Height to be dynamic with Respect to indexPath.row number
        let cellWidth = collectionView?.bounds.size.width
        //Now Simply return these CellWidth and Height, and you are Good to go.
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "YourCellIdentifier", for: indexPath)
        //You Can Pass TextField As Well as i have considered UITextView for now.
        let cellSize = self. calculateSize(cellTextView: cell.textView, cellText: yourArrayOfText[indexPath.row], withFixedWidth: cellWidth)
        return cellSize
    }

And do not change the Cell Height by changing the Constraint.constant directly. Instead of this simply use Above Delegate method to change height. Changing Cell Constraint can cause issues like this.

Use bellow method to get your desired Size.

   func calculateSize(cellTextView: UITextView, cellText: String, withFixedWidth: CGFloat) -> CGSize {
            let textView = UITextView()
            textView.text = cellText
            textView.frame = cellTextView.frame
            textView.font = cellTextView.font
            textView.tag = cellTextView.tag
            let fixedWidth = withFixedWidth
            textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
            var newSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
            return newSize
        }

Use Bellow method to calculate the image size.

//Method to Calculate ImageSize.
func calculateImageSize(image: UIImage) -> CGSize {
        var newSize = image.size
        if newSize.width > (ChatView.maxWidth - 70) {
            newSize.width = ChatView.maxWidth - 70
            newSize.height = ChatView.maxWidth - 70
        }
        if newSize.height < 60 {
            newSize.height = 60
            newSize.width = 60
        }
        return newSize
    }

Hope this helps!