Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calendar-like UICollectionView - how to add left inset before first item only?

I have the following UICollectionView: It has vertical scrolling, 1 section and 31 items. It has the basic setup and I am calculating itemSize to fit exactly 7 per row. Currently it looks like this:

enter image description here

However, I would like to make an inset before first item, so that the layout is even and there are the same number of items in first and last row. This is static and will always contain 31 items, so I am basically trying to add left space/inset before first item, so that it looks like this:

enter image description here

I have tried using a custom UICollectionViewDelegateFlowLayout method:

collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int)

But since there is only 1 section with 31 rows, it insets all of the rows, not just the first. I know I could probably add two more "blank" items, but I feel like there is a better solution I may not be aware of. Any ideas?

EDIT: I've tried Tarun's answer, but this doesn't work. Origin of first item changes, but the rest stays as is, therefore first overlaps the second and the rest remain as they were. Shifting them all doesn't work either. I ended up with:

enter image description here

like image 919
Fengson Avatar asked Feb 04 '26 20:02

Fengson


2 Answers

You need to subclass UICollectionViewFlowLayout and that will provide you a chance to customize the frame for all items within the collectionView.

import UIKit

class LeftAlignCellCollectionFlowLayout: UICollectionViewFlowLayout {
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }
        guard let collectionView = self.collectionView else { return nil }
        
        var newAttributes: [UICollectionViewLayoutAttributes] = []
        let leftMargin = self.sectionInset.left

        let layout = collectionView.collectionViewLayout
        
        for attribute in attributes {
            if let cellAttribute = layout.layoutAttributesForItem(at: attribute.indexPath) {
                // Check for `indexPath.item == 0` & do what you want
                // cellAttribute.frame.origin.x = 0 // 80
                
                newAttributes.append(cellAttribute)
            }
        }
        
        return newAttributes
    }
}

Now you can use this custom layout class as your flow layout like following.

let flowLayout = LeftAlignCellCollectionFlowLayout()
collectionView.collectionViewLayout = flowLayout
like image 54
Tarun Tyagi Avatar answered Feb 06 '26 13:02

Tarun Tyagi


Following Taran's suggestion, I've decided to use a custom UICollectionViewFlowLayout. Here is a generic answer that works for any number of items in the collectionView, as well as any inset value:

override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
    guard let collectionView = self.collectionView else { return nil }
    guard let attributes = super.layoutAttributesForElements(in: rect) else { return nil }

    var newAttributes: [UICollectionViewLayoutAttributes] = []

    for attribute in attributes {
        if let cellAttribute = collectionView.collectionViewLayout.layoutAttributesForItem(at: attribute.indexPath) {
            let itemSize = cellAttribute.frame.size.width + self.minimumInteritemSpacing
            let targetOriginX = cellAttribute.frame.origin.x + CGFloat(self.itemInset) * itemSize

            if targetOriginX <= collectionView.bounds.size.width {
                cellAttribute.frame.origin.x = targetOriginX
            } else {
                let shiftedPosition = lround(Double((targetOriginX / itemSize).truncatingRemainder(dividingBy: CGFloat(self.numberOfColumns))))

                cellAttribute.frame.origin.x = itemSize * CGFloat(shiftedPosition)
                cellAttribute.frame.origin.y += itemSize
            }

            newAttributes.append(cellAttribute)
        }
    }

    return newAttributes
}

where:

  • self.itemInset is the value we want to inset from the left (2 for my initial question, but it can be any number from 0 to the number of columns-1)
  • self.numberOfColumns is - as the name suggests - number of columns in the collectionView. This pertains to the number of days in my example and would always be equal to 7, but one might want this to be a generic value for some other use case.

Just for the sake of the completeness, I provide a method that calculates a size for my callendar collection view, based on the number of columns (days):

private func collectionViewItemSize() -> CGSize {
    let dimension = self.collectionView.frame.size.width / CGFloat(Constants.numberOfDaysInWeek) - Constants.minimumInteritemSpacing
    return CGSize(width: dimension, height: dimension)
}

For me, Constants.numberOfDaysInWeek is naturally 7, and Constants.minimumInteritemSpacing is equal to 2, but those can be any numbers you desire.

like image 29
Fengson Avatar answered Feb 06 '26 13:02

Fengson



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!