LoginSignup
28
26

More than 3 years have passed since last update.

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

Posted at

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

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

名称未設定.png

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

レイアウトの考え方

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

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

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

実装サンプル

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

まとめ

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

28
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
26