iOS 13が登場して1年が経過しました。(もうiOS 14も出ますね)
昨年登場した物としてSwiftUIやCombineは注目度が高かったですがUICollectionViewCompositionalLayout
も忘れてはいけない存在です。
UICollectionViewCompositionalLayout
は簡単に柔軟なレイアウトを構築することができるので使い始めるとかなりの画面で役に立ちます。
しかし、少し凝ったレイアウトをしようと思うとどうすればいいのか分からなかったので、今回はPinterest風のレイアウトをどう実現するのかを考えてみました。
Waterfallレイアウト(Pinterest風レイアウト)とは
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に置いています。
まとめ
NSCollectionLayoutGroup.custom
を利用することで柔軟なレイアウトも簡単に構築することができました。
NSCollectionLayoutGroupCustomItem
はframe
の他にもzIndex
を指定することができるので、色々と柔軟なレイアウトが作れそうですね。