类似日历的 UICollectionView - 如何仅在第一项之前添加左插图?

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

我有以下 UICollectionView: 它具有 vertical 滚动、1 section31 items。 它具有基本设置,我正在计算 itemSize 以完全适合 7 per row。 目前看起来像这样:

不过,我想在第一个项目之前插入一个,这样布局就均匀了,而且第一行和最后一行的项目数量相同。这是静态的,将始终包含 31 个项目,所以我基本上是尝试在第一个项目之前添加 left space/inset,这样它看起来像这样:

我试过使用自定义 UICollectionViewDelegateFlowLayout 方法:

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

但是由于只有一个部分有 31 行,它会插入所有行,而不仅仅是第一行。 我知道我可以再添加两个“空白”项目,但我觉得有一个我可能不知道的更好的解决方案。有什么想法吗?

编辑:我试过 Tarun 的回答,但这不起作用。第一项的来源发生变化,但其余的保持原样,因此首先与第二项重叠,其余的保持原样。将它们全部转移也不起作用。我最终得到:

您需要子class UICollectionViewFlowLayout,这将使您有机会为 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
    }
}

现在您可以使用此自定义布局 class 作为您的流程布局,如下所示。

let flowLayout = LeftAlignCellCollectionFlowLayout()
collectionView.collectionViewLayout = flowLayout

根据 Taran 的建议,我决定使用自定义 UICollectionViewFlowLayout。这是适用于 collectionView 中任意数量的项目以及任意插入值的通用答案:

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
}

其中:

  • self.itemInset 是我们要从左边插入的值(我最初的问题是 2,但它可以是从 0number of columns-1 之间的任何数字)
  • self.numberOfColumns 是——顾名思义——collectionView 中的列数。这与我的示例中的天数有关,并且始终等于 7,但人们可能希望这是其他一些用例的通用值。

为了完整起见,我提供了一种根据列数(天)计算我的 callendar 集合视图大小的方法:

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

对我来说,Constants.numberOfDaysInWeek 自然是 7,而 Constants.minimumInteritemSpacing 等于 2,但这些可以是您想要的任何数字。