Skip to content

Instantly share code, notes, and snippets.

@breeno
Last active October 31, 2022 12:57
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save breeno/f16330c5ef06075b0fc476c65d9b00d8 to your computer and use it in GitHub Desktop.
Save breeno/f16330c5ef06075b0fc476c65d9b00d8 to your computer and use it in GitHub Desktop.
Simple take on a compositional layout with 2 column variable height items waterfall
import UIKit
class ViewController: UIViewController {
enum Section {
case main
}
struct Item: Hashable {
let height: CGFloat
let color: UIColor
private let identifier = UUID()
func hash(into hasher: inout Hasher) {
hasher.combine(identifier)
}
static func == (lhs:Item, rhs:Item) -> Bool {
return lhs.identifier == rhs.identifier
}
}
var currentSnapshot: NSDiffableDataSourceSnapshot<Section, Item>!
var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
var collectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
configureHierarchy()
configureDataSource()
}
}
extension ViewController {
func configureHierarchy() {
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: twoColumnWaterfallLayout())
collectionView.autoresizingMask = [.flexibleWidth, .flexibleWidth]
view.addSubview(collectionView)
}
func configureDataSource() {
let reuseIdentifier = "cell-idententifier"
collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, item: Item) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath)
cell.contentView.backgroundColor = item.color
cell.contentView.layer.borderColor = UIColor.black.cgColor
cell.contentView.layer.borderWidth = 1
return cell
}
currentSnapshot = intialSnapshot()
dataSource.apply(currentSnapshot, animatingDifferences: false)
}
func intialSnapshot() -> NSDiffableDataSourceSnapshot<Section, Item> {
let itemCount = 200
var items = [Item]()
for _ in 0..<itemCount {
let height = CGFloat.random(in: 88..<121)
let color = UIColor(hue:CGFloat.random(in: 0.1..<0.9), saturation: 1.0, brightness: 1.0, alpha: 1.0)
let item = Item(height: height, color: color)
items.append(item)
}
let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.main])
snapshot.appendItems(items)
return snapshot
}
// +----------------------+ +----------------------+
// |Leading Vertical Group| | Trailing Vertical |
// +----------------------+ +----------------------+
// | |
// | |
// | |
// v v
// +--------------------------+--------------------------+
// | | |
// | | |
// | | |
// +--------------------------+--------------------------+
// | | |
// +--------------------------+ |
// | | |
// | +--------------------------+
// | | |
// | | |
// +--------------------------+ |
// | | |
// | | |
// | | |
// +--------------------------+--------------------------+
// | | |
// | | | +-----------------------------+
// +--------------------------+--------------------------+ <----------| Horizontal Container Group |
// | | | +-----------------------------+
// | | |
// | +--------------------------+
// +--------------------------+ |
// | | |
// | | |
// +--------------------------+--------------------------+
// | | |
// | | |
// | +--------------------------+
// +--------------------------+ |
// | | |
// | | |
// | +--------------------------+
// +--------------------------+ |
// | | |
// | | |
// +--------------------------+--------------------------+
//
//
//
// +---------------------------------------------------------------------------------------------------------+
// |*Container group is horizontal with Leading + Trailing vertical groups |
// | |
// |* Alternate between the leading + trailing group adding items from metadata about each item's height |
// | |
// |* When updates occur, the sectionProvider is involved |
// | |
// |* This is still very fast: the definitions are very cheap to create and the layout is optimized for even |
// |large groups |
// | |
// +---------------------------------------------------------------------------------------------------------+
func twoColumnWaterfallLayout() -> UICollectionViewLayout {
let sectionProvider = { [weak self] (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
guard let self = self else { return nil }
var leadingGroupHeight = CGFloat(0.0)
var trailingGroupHeight = CGFloat(0.0)
var leadingGroupItems = [NSCollectionLayoutItem]()
var trailingGroupItems = [NSCollectionLayoutItem]()
let items = self.currentSnapshot.itemIdentifiers
let totalHeight = items.reduce(0) { $0 + $1.height }
let columnHeight = CGFloat(totalHeight / 2.0)
// could get a bit fancier and balance the columns if they are too different height-wise - here is just a simple take on this
var runningHeight = CGFloat(0.0)
for index in 0..<self.currentSnapshot.numberOfItems {
let item = items[index]
let isLeading = runningHeight < columnHeight
let layoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(item.height))
let layoutItem = NSCollectionLayoutItem(layoutSize: layoutSize)
runningHeight += item.height
if isLeading {
leadingGroupItems.append(layoutItem)
leadingGroupHeight += item.height
} else {
trailingGroupItems.append(layoutItem)
trailingGroupHeight += item.height
}
}
let leadingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(leadingGroupHeight))
let leadingGroup = NSCollectionLayoutGroup.vertical(layoutSize: leadingGroupSize, subitems:leadingGroupItems)
let trailingGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.5), heightDimension: .absolute(trailingGroupHeight))
let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: trailingGroupSize, subitems: trailingGroupItems)
let containerGroupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(max(leadingGroupHeight, trailingGroupHeight)))
let containerGroup = NSCollectionLayoutGroup.horizontal(layoutSize: containerGroupSize, subitems: [leadingGroup, trailingGroup])
let section = NSCollectionLayoutSection(group: containerGroup)
return section
}
let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider)
return layout
}
}
@breeno
Copy link
Author

breeno commented Jun 29, 2019

Example result
twocollumnwaterfall

@peterwarbo
Copy link

Hi!

Can the above layout be achieved if you don't know the height beforehand (ie if you are fetching an image from a url)?

@almas73
Copy link

almas73 commented Feb 17, 2021

One problem with this layout is that it lays out cells top-to-bottom, not left-to-right, so its not really a waterfall:
Screen Shot 2021-02-16 at 10 50 07 PM

@peterwarbo
Copy link

@almas73 How could we modify it to be left-to-right?

One problem with this layout is that it lays out cells top-to-bottom, not left-to-right, so its not really a waterfall:
Screen Shot 2021-02-16 at 10 50 07 PM

@halpz
Copy link

halpz commented Nov 10, 2021

you could do something like this:
func splitItems() -> ([String],[String]){ var array1: [String] = [] var array2: [String] = [] for (index, item) in items.enumerated() { if index % 2 == 0 { array1.append(item) } else { array2.append(item) } } return (array1, array2) }

@halpz
Copy link

halpz commented Nov 10, 2021

then in cellForItem, do this:

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as? Cell { var item: String let (array1, array2) = splitItems() if array1.indices.contains(indexPath.item) { item = array1[indexPath.item] } else { item = array2[indexPath.item-array1.count] } cell.titleLabel.text = "\(item)" return cell } return UICollectionViewCell() }

@halpz
Copy link

halpz commented Nov 10, 2021

also use it in the layout generator , putting array one in the leading items and array2 in the trailing

@vebbis321
Copy link

Has anyone solved this? I could only find examples where the height is predetermined.

@vebbis321
Copy link

After a ton of research, I actually found a layout that works. Check out https://github.com/eeshishko/WaterfallTrueCompositionalLayout

Creds to him, I also tested it and it worked. I'm adding a demo to the project shortly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment