UICollectionView Custom Layout Middle column starting higher

Viewed 440

I have a UICollectionView scrolling vertically right now. I would like to have it look like this =, where the middle row starts a little higher than the other two to create a cool and interesting effect. Any ideas on how to achieve this?

enter image description here

1 Answers

The Collection view layout can be achieved by creating a custom layout class.

Theory:

Basically, collection view works directly with the custom layout object to manage the overall layout process, asking for the required layout information.

During the layout process, the collection view calls specific methods of the layout object. These methods provide a chance to calculate the position of items and to provide the collection view with the primary information it needs.

Following methods are always called in order during the layout process:

  1. prepare(): Perform the up-front calculations needed to provide layout information
  2. collectionViewContentSize: Return the overall size of the entire content area based on your initial calculations
  3. layoutAttributesForElements(in:): Return the attributes for cells and views that are in the specified rectangle

Practical:

  1. Prerequisite: Assuming we have a Collection view in place and configured with the datasource and delegates, let's create a UICollectionViewLayout subclass. I named it HiveLayout. I also assigned it to the collectionView in the storyboard. We also need some variable that will be useful in the process

    //    Properties for configuring the layout: the number of columns and the cell padding.
    fileprivate var numberOfColumns = 3
    fileprivate var cellPadding: CGFloat = 10
    
    //    Cache the calculated attributes. When you call prepare(), you’ll calculate the attributes for all items and add them to the cache. You can be efficient and query the cache instead of recalculating them every time.
    fileprivate var cache = [UICollectionViewLayoutAttributes]()
    
    //    Properties to store the content size.
    fileprivate var contentHeight: CGFloat = 0
    fileprivate var contentWidth: CGFloat {
        guard let collectionView = collectionView else {
            return 0
        }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }
    
  2. prepare(): This is where we will actually calculate the attributes of the cells

    override func prepare() {
        // If cache is empty and the collection view exists – calculate the layout attributes
        guard cache.isEmpty == true, let collectionView = collectionView else {
            return
        }
    
        // xOffset: array with the x-coordinate for every column based on the column widths
        // yOffset: array with the y-position for every column, Using odd-even logic to push the even cell upwards and odd cells down.
        let columnWidth = contentWidth / CGFloat(numberOfColumns)
        var xOffset = [CGFloat]()
        for column in 0 ..< numberOfColumns {
            xOffset.append(CGFloat(column) * columnWidth)
        }
        var column = 0
    
        var yOffset = [CGFloat]()
        for i in 0..<numberOfColumns {
            yOffset.append((i % 2 == 0) ? (columnWidth / 2) : 0)
        }
    
        for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
    
            let indexPath = IndexPath(item: item, section: 0)
    
            // Calculate insetFrame that can be set to the attribute
            let cellHeight = columnWidth - (cellPadding * 2)
            let height = cellPadding * 2 + cellHeight
            let frame = CGRect(x: xOffset[column], y: yOffset[column], width: columnWidth, height: height)
            let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
    
            // Create an instance of UICollectionViewLayoutAttribute, sets its frame using insetFrame and appends the attributes to cache.
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = insetFrame
            cache.append(attributes)
    
            // Update the contentHeight to account for the frame of the newly calculated item. It then advances the yOffset for the current column based on the frame
            contentHeight = max(contentHeight, frame.maxY)
            yOffset[column] = yOffset[column] + height
    
            column = column < (numberOfColumns - 1) ? (column + 1) : 0
        }
    }
    
  3. collectionViewContentSize:

    //    Using contentWidth and contentHeight, calculate collectionViewContentSize.
    override var collectionViewContentSize: CGSize {
        return CGSize(width: contentWidth, height: contentHeight)
    }
    
  4. layoutAttributesForElements(in:):

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    
        var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
    
        for attributes in cache {
            if attributes.frame.intersects(rect) {
                visibleLayoutAttributes.append(attributes)
            }
        }
        return visibleLayoutAttributes
    }
    
  5. layoutAttributesForItem(at:): To return attributes for particular cell

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cache[indexPath.item]
    }
    

Please check out the gist for the Layout Class.

Here it is in action!!

Demo

Related