You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Swift中如何使UICollectionView最后一行单元格居中?

嘿,这个需求我之前帮好几个开发者搞定过,在Swift里让UICollectionView最后一行居中其实没那么复杂,最稳妥的方式是自定义UICollectionViewFlowLayout——毕竟系统自带的FlowLayout没法直接实现这个效果。下面给你两种方案,你可以根据自己的场景挑:

方案一:自定义UICollectionViewFlowLayout(推荐)

这种方式最优雅,能自动适配屏幕旋转、section变化等场景,而且不需要在数据源代理里写额外逻辑。

实现步骤:

  1. 创建一个继承自UICollectionViewFlowLayout的自定义布局类
  2. 重写layoutAttributesForElements(in:)方法,计算最后一行cell的位置并调整它们的frame
  3. 重写shouldInvalidateLayout(forBoundsChange:)确保布局在屏幕旋转时重新计算

完整代码:

class CenteredLastRowFlowLayout: UICollectionViewFlowLayout {
    
    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let baseAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
        // 复制一份属性,避免修改系统原有的数据
        let attributes = baseAttributes.map { $0.copy() as! UICollectionViewLayoutAttributes }
        
        // 按section分组处理每个section的布局
        var sectionGroups: [[UICollectionViewLayoutAttributes]] = []
        var currentSection: [UICollectionViewLayoutAttributes] = []
        
        for attr in attributes {
            guard attr.representedElementCategory == .cell else { continue }
            
            if currentSection.isEmpty || currentSection.first?.indexPath.section == attr.indexPath.section {
                currentSection.append(attr)
            } else {
                sectionGroups.append(currentSection)
                currentSection = [attr]
            }
        }
        if !currentSection.isEmpty {
            sectionGroups.append(currentSection)
        }
        
        // 处理每个section的最后一行
        for sectionAttrs in sectionGroups {
            guard let collectionView = collectionView else { continue }
            let section = sectionAttrs.first?.indexPath.section ?? 0
            let totalItems = collectionView.numberOfItems(inSection: section)
            let insets = self.sectionInset
            let availableWidth = collectionView.bounds.width - insets.left - insets.right
            let itemWidth = self.itemSize.width
            let interItemSpacing = self.minimumInteritemSpacing
            
            // 计算一行能容纳的item数量
            let itemsPerRow = Int(floor((availableWidth + interItemSpacing) / (itemWidth + interItemSpacing)))
            guard itemsPerRow > 0 else { continue }
            
            // 如果section的item总数小于等于一行数量,直接居中所有item
            if totalItems <= itemsPerRow {
                let totalGap = availableWidth - (CGFloat(totalItems) * itemWidth) - (CGFloat(totalItems - 1) * interItemSpacing)
                let leftOffset = totalGap / 2
                
                for (index, attr) in sectionAttrs.enumerated() {
                    var frame = attr.frame
                    frame.origin.x = insets.left + leftOffset + (CGFloat(index) * (itemWidth + interItemSpacing))
                    attr.frame = frame
                }
            } else {
                // 计算最后一行的item数量
                let lastRowCount = totalItems % itemsPerRow
                guard lastRowCount > 0 else { continue }
                
                // 获取最后一行的所有cell属性
                let lastRowAttrs = sectionAttrs.suffix(lastRowCount)
                let lastRowTotalWidth = (CGFloat(lastRowCount) * itemWidth) + (CGFloat(lastRowCount - 1) * interItemSpacing)
                let leftOffset = (availableWidth - lastRowTotalWidth) / 2
                
                // 调整每个cell的x坐标实现居中
                for attr in lastRowAttrs {
                    var frame = attr.frame
                    frame.origin.x = insets.left + leftOffset + (CGFloat(attr.indexPath.item % itemsPerRow) * (itemWidth + interItemSpacing))
                    attr.frame = frame
                }
            }
        }
        
        return attributes
    }
    
    override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
        // 当collectionView的边界变化(比如旋转屏幕)时,重新计算布局
        return true
    }
}

使用方式:

创建UICollectionView时,直接使用这个自定义布局即可:

// 初始化自定义布局
let centeredLayout = CenteredLastRowFlowLayout()
centeredLayout.itemSize = CGSize(width: 80, height: 80) // 设置你的cell大小
centeredLayout.minimumInteritemSpacing = 12 // 设置item之间的间距
centeredLayout.sectionInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) // 设置section内边距

// 创建collectionView
let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: centeredLayout)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(YourCellClass.self, forCellWithReuseIdentifier: "CellID")
view.addSubview(collectionView)
方案二:在Cell显示时调整Frame(快速实现)

如果你不想自定义布局,也可以在cell即将显示时手动调整它的frame。这种方式代码量小,但适配性不如自定义布局(比如屏幕旋转时需要额外处理)。

实现代码:

在你的UICollectionViewDelegate里添加这个方法:

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
    guard let flowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
    
    let section = indexPath.section
    let totalItemsInSection = collectionView.numberOfItems(inSection: section)
    let insets = flowLayout.sectionInset
    let availableWidth = collectionView.bounds.width - insets.left - insets.right
    let itemWidth = flowLayout.itemSize.width
    let spacing = flowLayout.minimumInteritemSpacing
    
    let itemsPerRow = Int(floor((availableWidth + spacing) / (itemWidth + spacing)))
    guard itemsPerRow > 0 else { return }
    
    // 判断当前cell是否属于最后一行
    let lastRowItemCount = totalItemsInSection % itemsPerRow
    if indexPath.item >= totalItemsInSection - lastRowItemCount {
        // 计算最后一行需要的左边偏移量
        let totalRowWidth = (CGFloat(lastRowItemCount) * itemWidth) + (CGFloat(lastRowItemCount - 1) * spacing)
        let leftOffset = (availableWidth - totalRowWidth) / 2
        
        // 调整cell的frame
        var cellFrame = cell.frame
        cellFrame.origin.x = insets.left + leftOffset + (CGFloat(indexPath.item % itemsPerRow) * (itemWidth + spacing))
        cell.frame = cellFrame
    }
}
注意事项
  • 如果你的cell是动态大小(比如根据内容计算宽度),需要在自定义布局里额外处理itemSize的计算逻辑
  • 自定义布局支持多section,每个section的最后一行都会自动居中
  • 如果使用方案二,记得在屏幕旋转时调用collectionView.reloadData()来重新调整cell位置

内容的提问来源于stack exchange,提问作者EnasAZ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 03:45:29