Search code examples
swiftuicollectionviewuicollectionviewlayout

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


Solution

  • 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.