Swift 4中UICollectionView垂直流布局实现不同宽高单元格问题求助
你想要实现类似参考图的不规则图片网格布局,但当前代码的效果和预期不符,我来帮你拆解问题并给出修复方案:
你的代码存在的核心问题
1. 多余的 cell.frame 手动修改
你在 cellForItemAt 里手动设置了 cell 的 frame:
cell.frame = CGRect(x: cell.frame.origin.x, y: cell.frame.origin.y , width: widthss[indexPath.row], height: heights[indexPath.row])
这完全是画蛇添足!UICollectionViewDelegateFlowLayout 协议里的 sizeForItemAt 方法已经负责告诉 CollectionView 每个 cell 的尺寸了,手动修改 frame 会干扰系统的布局逻辑,尤其是在 cell 复用的时候,很容易导致布局错乱。直接删掉这行代码就好。
2. UICollectionViewFlowLayout 不支持你要的不规则布局
默认的 UICollectionViewFlowLayout 是流式布局,它的核心逻辑是把 cell 按行排列,每行的所有 cell 会自动对齐到同一基线(取该行最高 cell 的高度作为整行高度)。但你要的是错落有致的不规则网格(类似瀑布流),这种布局逻辑是 FlowLayout 默认做不到的——它只会把高的 cell 撑高整行,而不会让其他行的 cell 填补空白。
修复方案
要实现你想要的不规则网格,有两种可行的方式:
方案一:自定义 UICollectionViewLayout
自己实现一个布局类,手动计算每个 cell 的位置,核心思路是跟踪每列的当前高度,把新 cell 放在高度最低的列上,模拟瀑布流效果。示例代码如下:
class CustomWaterfallLayout: UICollectionViewLayout { // 可配置参数 var columnCount = 3 // 根据你的布局需求设置列数 var itemSpacing: CGFloat = 1.0 var lineSpacing: CGFloat = 1.0 var itemSizes: [CGSize] = [] // 存储每个cell的预设尺寸 // 内部缓存变量 private var layoutAttributesCache: [UICollectionViewLayoutAttributes] = [] private var contentTotalHeight: CGFloat = 0 private var contentWidth: CGFloat { guard let collectionView = collectionView else { return 0 } let insets = collectionView.contentInset return collectionView.bounds.width - (insets.left + insets.right) } override func prepare() { super.prepare() guard layoutAttributesCache.isEmpty, let collectionView = collectionView else { return } let columnWidth = (contentWidth - (CGFloat(columnCount - 1) * itemSpacing)) / CGFloat(columnCount) var columnXOffsets: [CGFloat] = [] for column in 0..<columnCount { columnXOffsets.append(CGFloat(column) * (columnWidth + itemSpacing)) } var columnYOffsets: [CGFloat] = Array(repeating: 0, count: columnCount) // 遍历所有cell,计算每个cell的frame for itemIndex in 0..<collectionView.numberOfItems(inSection: 0) { let indexPath = IndexPath(item: itemIndex, section: 0) let targetSize = itemSizes[itemIndex] // 找到当前高度最低的列,用来放置新cell let targetColumn = columnYOffsets.enumerated().min { $0.element < $1.element }?.offset ?? 0 let xPos = columnXOffsets[targetColumn] let yPos = columnYOffsets[targetColumn] let cellFrame = CGRect(x: xPos, y: yPos, width: targetSize.width, height: targetSize.height) let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) attributes.frame = cellFrame layoutAttributesCache.append(attributes) // 更新列高度和整体内容高度 contentTotalHeight = max(contentTotalHeight, cellFrame.maxY) columnYOffsets[targetColumn] = cellFrame.maxY + lineSpacing } } override var collectionViewContentSize: CGSize { return CGSize(width: contentWidth, height: contentTotalHeight) } override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { return layoutAttributesCache.filter { $0.frame.intersects(rect) } } override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { return layoutAttributesCache[indexPath.item] } }
然后在你的 ViewController 里替换布局:
override func viewDidLoad() { super.viewDidLoad() // 初始化自定义布局 let customLayout = CustomWaterfallLayout() customLayout.columnCount = 3 // 匹配你的布局列数 customLayout.itemSpacing = 1.0 customLayout.lineSpacing = 1.0 // 把你的宽高数组转换成CGSize数组 customLayout.itemSizes = zip(widthss, heights).map { CGSize(width: $0, height: $1) } // 给CollectionView设置自定义布局 collectionViw.collectionViewLayout = customLayout // 确保cell已注册(如果xib里没设置的话) collectionViw.register(UICustomCollectionViewCell.self, forCellWithReuseIdentifier: "cell") }
方案二:使用第三方布局库
如果不想自己写布局逻辑,可以直接用成熟的第三方库,比如 PinterestLayout 或者 UICollectionViewWaterfallLayout,它们已经封装好了不规则网格的布局逻辑,你只需要传入每个 cell 的尺寸就能快速实现效果。
额外注意事项
- 确保你的
widthss数组里的宽度值符合列数分配,比如3列布局的话,单个cell的宽度应该接近列宽,跨列的cell宽度是列宽的倍数,避免出现超出屏幕或留白过多的问题。 - 检查
UICustomCollectionViewCell的内部约束,比如让内部的图片视图铺满cell,避免内容显示异常。
内容的提问来源于stack exchange,提问作者Ahmad Taalab

