如何用Decorator View实现UICollectionView目标设计及添加内部分隔线
嘿,我来帮你搞定这几个UICollectionView的问题,都是实际开发中经常碰到的场景,咱们逐个拆解:
一、添加内部分隔线
推荐两种靠谱的实现方式,按需选择:
方式1:自定义Cell时内置分隔线
这种方式最直观,在Cell的contentView里添加UIView作为分隔线,然后根据Cell的位置动态控制显示/隐藏:
class CustomCollectionViewCell: UICollectionViewCell { // 左分隔线、底部分隔线(按需添加右/上分隔线) let leftSeparator = UIView() let bottomSeparator = UIView() override init(frame: CGRect) { super.init(frame: frame) setupSeparators() } required init?(coder: NSCoder) { super.init(coder: coder) setupSeparators() } private func setupSeparators() { leftSeparator.backgroundColor = .lightGray bottomSeparator.backgroundColor = .lightGray contentView.addSubview(leftSeparator) contentView.addSubview(bottomSeparator) // 布局分隔线 leftSeparator.translatesAutoresizingMaskIntoConstraints = false bottomSeparator.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ leftSeparator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), leftSeparator.topAnchor.constraint(equalTo: contentView.topAnchor), leftSeparator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), leftSeparator.widthAnchor.constraint(equalToConstant: 0.5), bottomSeparator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), bottomSeparator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), bottomSeparator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomSeparator.heightAnchor.constraint(equalToConstant: 0.5) ]) } // 根据位置更新分隔线显示状态 func updateSeparators(for indexPath: IndexPath, itemsPerRow: Int, totalItemsInSection: Int) { // 左分隔线:第一列隐藏,其余显示 leftSeparator.isHidden = indexPath.item % itemsPerRow == 0 // 底部分隔线:最后一行隐藏,其余显示 let lastRowStartIndex = totalItemsInSection - (totalItemsInSection % itemsPerRow) let adjustedLastRowStart = totalItemsInSection % itemsPerRow == 0 ? lastRowStartIndex - itemsPerRow : lastRowStartIndex bottomSeparator.isHidden = indexPath.item >= adjustedLastRowStart } }
然后在cellForItemAt里调用这个方法:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath) as! CustomCollectionViewCell let itemsPerRow = calculateItemsPerRow() // 这个方法后面讲动态行数量时实现 let totalItems = collectionView.numberOfItems(inSection: indexPath.section) cell.updateSeparators(for: indexPath, itemsPerRow: itemsPerRow, totalItemsInSection: totalItems) // 其他Cell配置逻辑 return cell }
方式2:自定义FlowLayout绘制分隔线
如果不想侵入Cell,也可以在布局里直接画分隔线,重写draw(_ rect:)方法:
class CustomFlowLayout: UICollectionViewFlowLayout { override func draw(_ rect: CGRect) { super.draw(rect) guard let collectionView = collectionView else { return } let itemsPerRow = calculateItemsPerRow() let sections = 0..<collectionView.numberOfSections for section in sections { let totalItems = collectionView.numberOfItems(inSection: section) guard totalItems > 0 else { continue } // 计算最后一行的起始位置 let lastRowStartIndex = totalItems - (totalItems % itemsPerRow) let adjustedLastRowStart = totalItems % itemsPerRow == 0 ? lastRowStartIndex - itemsPerRow : lastRowStartIndex for item in 0..<totalItems { let indexPath = IndexPath(item: item, section: section) guard let attributes = layoutAttributesForItem(at: indexPath) else { continue } let cellFrame = attributes.frame // 绘制左分隔线(第一列跳过) if item % itemsPerRow != 0 { let leftLineFrame = CGRect(x: cellFrame.minX, y: cellFrame.minY, width: 0.5, height: cellFrame.height) UIColor.lightGray.setFill() UIRectFill(leftLineFrame) } // 绘制底部分隔线(最后一行跳过) if item < adjustedLastRowStart { let bottomLineFrame = CGRect(x: cellFrame.minX, y: cellFrame.maxY, width: cellFrame.width, height: 0.5) UIColor.lightGray.setFill() UIRectFill(bottomLineFrame) } } } } // 布局变化时重新绘制分隔线 override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { return true } }
二、实现每行Cell数量动态变化
核心是根据CollectionView的宽度(或屏幕尺寸、自定义规则)实时计算每行能容纳的Cell数量,然后更新ItemSize。自定义FlowLayout是最佳实践:
class DynamicFlowLayout: UICollectionViewFlowLayout { // 可以自定义最小Cell宽度,适配不同屏幕 private let minItemWidth: CGFloat = 150 override func prepare() { super.prepare() guard let collectionView = collectionView else { return } let itemsPerRow = calculateItemsPerRow() let availableWidth = collectionView.bounds.width - sectionInset.left - sectionInset.right - minimumInteritemSpacing * CGFloat(itemsPerRow - 1) let itemWidth = floor(availableWidth / CGFloat(itemsPerRow)) // 固定高度或根据内容动态计算高度 itemSize = CGSize(width: itemWidth, height: 100) } private func calculateItemsPerRow() -> Int { guard let collectionView = collectionView else { return 1 } let totalAvailableWidth = collectionView.bounds.width - sectionInset.left - sectionInset.right // 计算最多能放多少个Cell(每个Cell至少minItemWidth,加上间距) let maxItems = Int(floor((totalAvailableWidth + minimumInteritemSpacing) / (minItemWidth + minimumInteritemSpacing))) return max(maxItems, 1) // 至少显示1个 } // 屏幕旋转或尺寸变化时重新计算布局 override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { guard let collectionView = collectionView else { return false } return newBounds.width != collectionView.bounds.width } }
然后给CollectionView设置这个布局:
let layout = DynamicFlowLayout() layout.minimumInteritemSpacing = 10 layout.minimumLineSpacing = 10 layout.sectionInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) collectionView.collectionViewLayout = layout
三、最后一行隐藏底部分隔线
这个逻辑其实已经包含在上面的两个分隔线实现里了,核心是判断当前Cell是否属于所在Section的最后一行:
- 先计算当前Section的总Item数
totalItems - 根据每行数量
itemsPerRow,算出最后一行的起始Item索引:- 如果
totalItems % itemsPerRow == 0,说明最后一行刚好填满,起始索引是totalItems - itemsPerRow - 否则起始索引是
totalItems - (totalItems % itemsPerRow)
- 如果
- 只要当前Cell的
indexPath.item >= 最后一行起始索引,就隐藏底部分隔线
比如在Cell的updateSeparators方法里已经实现了这个判断,自定义FlowLayout的绘制逻辑里也做了同样的判断,直接复用即可。
内容的提问来源于stack exchange,提问作者Jaydeep Vyas
相关产品推荐
相关产品推荐

