Swift中如何使UICollectionView最后一行单元格居中?
嘿,这个需求我之前帮好几个开发者搞定过,在Swift里让UICollectionView最后一行居中其实没那么复杂,最稳妥的方式是自定义UICollectionViewFlowLayout——毕竟系统自带的FlowLayout没法直接实现这个效果。下面给你两种方案,你可以根据自己的场景挑:
方案一:自定义UICollectionViewFlowLayout(推荐)
这种方式最优雅,能自动适配屏幕旋转、section变化等场景,而且不需要在数据源代理里写额外逻辑。
实现步骤:
- 创建一个继承自
UICollectionViewFlowLayout的自定义布局类 - 重写
layoutAttributesForElements(in:)方法,计算最后一行cell的位置并调整它们的frame - 重写
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
相关产品推荐
相关产品推荐

