-
-
Save breeno/f16330c5ef06075b0fc476c65d9b00d8 to your computer and use it in GitHub Desktop.
| 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 | |
| } | |
| } | |
@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:
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) }
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() }
also use it in the layout generator , putting array one in the leading items and array2 in the trailing
Has anyone solved this? I could only find examples where the height is predetermined.
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.

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)?