Search code examples
iosswiftuicollectionviewflowlayoutuicollectionviewflowlayout

Autoresize UICollectionView cells, Align cell to top


I've a UICollectionView, with multiple sections and rows. Header and Footer views wherever necessary, of fixed size. Cells that are autoresizable

The cell view are designed like :

Green colour - ImageView

Orange colour - Labels with numberOfLines = 0.

The cell should expand it's size according to label numberOfLines.

I've achieved this using this code in MyCustomCell :

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
        super.apply(layoutAttributes)
        let autoLayoutAttributes = super.preferredLayoutAttributesFitting(layoutAttributes)
        let targetSize = CGSize(width: Constants.screenWidth/3.5, height: 0)
        let autoLayoutSize = contentView.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: UILayoutPriority.required, verticalFittingPriority: UILayoutPriority.defaultLow)
        let autoLayoutFrame = CGRect(origin: autoLayoutAttributes.frame.origin, size: autoLayoutSize)
        autoLayoutAttributes.frame = autoLayoutFrame
        return autoLayoutAttributes
    }

The cells are autoresizing but the contentView (in cyan colour) are Centre aligned both vertically and horizontally.

I need to make it vertically align to Top.

I had the alignment problem with headers and footers too. For that i've subclassed UICollectionViewFlowLayout


class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {

    override func invalidationContext(forPreferredLayoutAttributes preferredAttributes: UICollectionViewLayoutAttributes, withOriginalAttributes originalAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutInvalidationContext {

        let context: UICollectionViewLayoutInvalidationContext = super.invalidationContext(forPreferredLayoutAttributes: preferredAttributes, withOriginalAttributes: originalAttributes)

        let indexPath = preferredAttributes.indexPath
        context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter, at: [indexPath])
        context.invalidateSupplementaryElements(ofKind: UICollectionView.elementKindSectionHeader, at: [indexPath])

        return context
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        let attributes = super.layoutAttributesForElements(in: rect)

        var topMargin = sectionInset.top
        var leftMargin = sectionInset.left
        var maxY: CGFloat = -1.0
        attributes?.forEach { layoutAttribute in
            guard layoutAttribute.representedElementCategory == .cell else {
                return
            }

            if layoutAttribute.frame.origin.y >= maxY {
                leftMargin = sectionInset.left
                topMargin = sectionInset.top
            }

            layoutAttribute.frame.origin.x = leftMargin

            leftMargin += layoutAttribute.frame.width + minimumInteritemSpacing
            maxY = max(layoutAttribute.frame.maxY , maxY)
        }

        return attributes
    }

}

Here is an image to illustrate current situation. The cyan are contentView of cells. I've to make it align to Top.

enter image description here

EDIT: So i realised that UICollectionViewFlowLayout code was creating more bugs than fixing a problem. I added layoutAttributesForElements to left align my cell in case there is only one cell. What it actually did was align all of my cells to the left. Modified the code as


class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {

        let attributes = super.layoutAttributesForElements(in: rect)

        if attributes?.count == 1 {

            if let currentAttribute = attributes?.first {
                currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
            }
        }
        return attributes
    }
}

Now my UICollectionViewCell are properly aligned to horizontal centre with exception to if only one cell which will be left aligned.

Still no solution for vertical alignment.


Solution

  • So I worked around this for quite a bit. No answers from stack overflow help in any way. So i played with custom UICollectionViewFlowLayout method layoutAttributesForElements.

    This method will call for every section, and will have layoutAttributesForElements for rect. This will give an array of UICollectionViewLayoutAttributes which one can modify as per the need.

    I was having problem figuring out the y coordinate which I would like to change for my cells.

    So first with a loop in attributes from layoutAttributesForElements I got the header and saved it's height in a variable. Then changed the rest of cell's y coordinate with the headerHeight value.

    Not sure if this is the right way or not, nor did any performance testing. For now everything seems fine without any lag or jerk.

    Here is full code or custom UICollectionViewFlowLayout.

    
    class MainCollectionViewFlowLayout: UICollectionViewFlowLayout {
        override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
            let attr = super.layoutAttributesForElements(in: rect)
            var attributes = [UICollectionViewLayoutAttributes]()
            for itemAttributes in attr! {
                let itemAttributesCopy = itemAttributes.copy() as! UICollectionViewLayoutAttributes
                // manipulate itemAttributesCopy
                attributes.append(itemAttributesCopy)
            }
    
            if attributes.count == 1 {
    
                if let currentAttribute = attributes.first {
                    currentAttribute.frame = CGRect(x: self.sectionInset.left, y: currentAttribute.frame.origin.y, width: currentAttribute.frame.size.width, height: currentAttribute.frame.size.height)
                }
            } else {
                var sectionHeight: CGFloat = 0
    
                attributes.forEach { layoutAttribute in
                    guard layoutAttribute.representedElementCategory == .supplementaryView else {
                        return
                    }
    
                    if layoutAttribute.representedElementKind == UICollectionView.elementKindSectionHeader {
                        sectionHeight = layoutAttribute.frame.size.height
                    }
                }
    
                attributes.forEach { layoutAttribute in
                    guard layoutAttribute.representedElementCategory == .cell else {
                        return
                    }
    
                    if layoutAttribute.frame.origin.x == 0 {
                        sectionHeight = max(layoutAttribute.frame.minY, sectionHeight)
                    }
    
                    layoutAttribute.frame = CGRect(origin: CGPoint(x: layoutAttribute.frame.origin.x, y: sectionHeight), size: layoutAttribute.frame.size)
    
                }
            }
            return attributes
        }
    }
    
    

    NOTE: You need to copy the attributes in an array first to work with. Else xcode will yell Logging only once for UICollectionViewFlowLayout cache mismatched frame in console.

    If any new/perfect solution is there please let me know.

    CHEERS!!!