UICollectionViewCompositionalLayoutでWaterfall(Pinterest風)レイアウトを実現する

iOS 13が登場して１年が経過しました。（もうiOS 14も出ますね）
昨年登場した物としてSwiftUIやCombineは注目度が高かったですが`UICollectionViewCompositionalLayout`も忘れてはいけない存在です。
`UICollectionViewCompositionalLayout`は簡単に柔軟なレイアウトを構築することができるので使い始めるとかなりの画面で役に立ちます。
しかし、少し凝ったレイアウトをしようと思うとどうすればいいのか分からなかったので、今回はPinterest風のレイアウトをどう実現するのかを考えてみました。

# Waterfallレイアウト(Pinterest風レイアウト)とは

![名称未設定.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2846/ce856f58-fde5-618c-8971-6d22ac6c60f0.png)

Waterfallレイアウトはこのように上からサイズが異なるコンポーネントが上から積み重なっているようなレイアウトのことを言います。
水が上から下に落ちるように見えるのでWaterfall(滝)ということですね。

# レイアウトの考え方

レイアウトを作る際に重要なのは次の2点です。

- カラム数（横に何列表示するか）
- それぞれのセルのサイズが明確なこと

上記が分かっていればカラムごとに高さを足していけばそれっぽいレイアウトが作れます。

# 実装サンプル

```swift
protocol WaterfallLayoutDelegate: AnyObject {
    func numberOfColumns() -> Int
    func columnsSize(at indexPath: IndexPath) -> CGSize
    func columnSpace() -> CGFloat
}

lazy var collectionViewLayout: UICollectionViewCompositionalLayout = {
    return UICollectionViewCompositionalLayout { [unowned self] (section, environment) -> NSCollectionLayoutSection? in
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(environment.container.effectiveContentSize.height))
        let group = NSCollectionLayoutGroup.custom(layoutSize: groupSize) { [unowned self] (environment) -> [NSCollectionLayoutGroupCustomItem] in
            var items: [NSCollectionLayoutGroupCustomItem] = []
            var layouts: [Int: CGFloat] = [:]
            let space: CGFloat = self.waterfallLayout.flatMap({ CGFloat($0.columnSpace()) }) ?? 1.0
            let numberOfColumn: CGFloat = self.waterfallLayout.flatMap({ CGFloat($0.numberOfColumns()) }) ?? 2.0
            let defaultSize = CGSize(width: 100, height: 100)

            (0 ..< self.collectionView.numberOfItems(inSection: section)).forEach {
                let indexPath = IndexPath(item: $0, section: section)

                let size = self.waterfallLayout?.columnsSize(at: indexPath) ?? defaultSize
                let aspect = CGFloat(size.height) / CGFloat(size.width)

                let width = (environment.container.effectiveContentSize.width - (numberOfColumn - 1) * space) / numberOfColumn
                let height = width * aspect

                let currentColumn = $0 % Int(numberOfColumn)
                let y = layouts[currentColumn] ?? 0.0 + space
                let x = width * CGFloat(currentColumn) + space * (CGFloat(currentColumn) - 1.0)

                let frame = CGRect(x: x, y: y + space, width: width, height: height)
                let item = NSCollectionLayoutGroupCustomItem(frame: frame)
                items.append(item)

                layouts[currentColumn] = frame.maxY
            }
            return items
        }
        return NSCollectionLayoutSection(group: group)
    }
}()

```

大事なところだけ抜き出しておきました。
フルのサンプルコードは以下のGistに置いています。

[fromkk/WaterfallLayout.swift](https://gist.github.com/fromkk/06eafd97fdb9f20c577a0d57ce527e95)

# まとめ

`NSCollectionLayoutGroup.custom`を利用することで柔軟なレイアウトも簡単に構築することができました。
`NSCollectionLayoutGroupCustomItem`は`frame`の他にも`zIndex`を指定することができるので、色々と柔軟なレイアウトが作れそうですね。