Goal:
UICollectionView with 'waterfall effect', entailing:
Multiple columns of cells (each cell contains only one stackview).
Each stackview contains n items, thus height of cells varies, but width is fixed.
Each cell in each column should butt up against one above it, with fixed padding, let's say
CGFloat(50.0)
.That produces a 'staggered' look across the columns, but no wasted space or 'empty slots'.
The Problem:
I've tried configuring the collection view using UICollectionViewFlowLayout
, and alternatively, UICollectionViewCompositionalLayout
without success
Best I've been able to achieve is cells aligned to top of each row, where each row has a uniform height (approx same height as tallest cell in that row). Meaning, next row starts below the bottom of the tallest cell in the previous row, leaving large gaps between shorter cells and cells placed cells in the next row (which is what I don't want).
Afterthoughts:
I suspect that's just how it is... that collection view (particularly compositional layout) doesn't know how to handle multiple columns with independent vertical layouts wherein all columns scroll vertically as one.
I'm not even sure if a custom layout could solve it, as I've never written one yet. Or if I'm just better off creating a big scroll view and layout the stackviews manually without a collection view, or multiple single-column collection views side by side? I don't understand enough of the tradeoffs and possibilities and hoping someone can steer me in the right direction.
Note, I did find this cool hack for making the content of cells top align for UICollectionViewFlow layout, and it works for what it is, but doesn't but doesn't give me the waterfall effect I'm looking e.g. doesn't tie multiple rows together for a seamless vertically sized cell height appearance.
UICollection View Flow Layout Vertical Align
class TopAlignedCollectionViewFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// This method is being requested to return the layout attributes for each cell in the rectangle.'
// where the attributes are frame, bounds, center, size, transform3D, transform, alpha, zIndix and isHidden
// It starts by getting a copy attributes the super class has configured each cell:
//
let attributes = super.layoutAttributesForElements(in: rect)?.map { $0.copy() } as? [UICollectionViewLayoutAttributes]
attributes?
// Then it's using the reduce() function to build a new dictionary of tupples consisting of:
// [cgfloat : (cgfloat, [attrs])], which is [ cell center.y : (cell origin.y, [all the other cells]) ]
///
.reduce([CGFloat: (CGFloat, [UICollectionViewLayoutAttributes])]()) { // for each cell, $0 = dict to build, $1 = next cell
guard $1.representedElementCategory == .cell else { return $0 } // only operate on cells (not decorations or supplemental views)
return $0.merging([ceil($1.center.y): ($1.frame.origin.y, [$1])]) { // center.y : origin.y, [merged attributes]
($0.0 < $1.0 ? $0.0 : $1.0, $0.1 + $1.1)
//
// if dict center.y < cell center.y
// (cell ctr.y, [cell/dict merged attributes]
// else
// (dict ctr.y, [cell/dict merged attributes]
}
} // returns dictionary of merged [cgfloat : (cgfloat, [attrs])]
.values.forEach { minY, line in // pull each item up to the top via diff btw center and top line
line.forEach {
$0.frame = $0.frame.offsetBy(
dx: 0,
dy: minY - $0.frame.origin.y
)
$0.frame = CGRectMake($0.frame.origin.x, $0.frame.origin.y + 200, $0.frame.size.width, 50) //$0.frame.size.height - (minY - $0.frame.origin.y))
}
}
return attributes
}
}