以前 potatotips #67 でこんな内容を発表しました。
その際「Itemの大きさが正確なGrid Layout」にする方法を紹介したのですが、「Item Spacingを正確にするには?」という課題の解決法を紹介したいと思います。
Item Sizeを揃えることとItem Spacingを揃えることは両立しない
Collection ViewのGrid Layoutは
- Collection View(Content Size)の幅
- 分割数
- 要素の間隔(Item Spacing)
によって各要素の frame
が決まりますが、
- 要素のItem Sizeを揃えること
- 要素間のItem Spacingを揃えること
は両立しない場合があります。
Item Sizeが「端数のない大きさ」でないと、スクリーン座標系にマップしたときに丸められ、要素のサイズないし要素の間隔に視覚的な「ずれ」が生じるためです。
デザイン要件としてどちらが重要なのかを確認し、それに準じた最適化を選択することになります。
NSCollectionLayoutGroup.custom()
による正確なItem Spacing
NSCollectionLayoutGroup.horizontal()
では、上記のような端数が生じるケースがあるため、正確なItem Spacingとならないことがあります。
そこで NSCollectionLayoutGroup.custom()
を利用し、要素の frame
を独自計算する方法を採ります。
extension NSCollectionLayoutSection {
/// Item Spacingが正確なレイアウトを生成する
/// - Parameters:
/// - count: 列数
/// - height: セルの高さ
/// - spacing: セルの間隔
static func column(count: Int, height: CGFloat, spacing: CGFloat) -> Self {
let group = NSCollectionLayoutGroup.custom(
layoutSize: .init(
widthDimension: .fractionalWidth(1.0),
heightDimension: .estimated(height)
)
) { layoutEnvironment in
let contentSize = layoutEnvironment.container.contentSize
let fCount = CGFloat(count)
// Item Spacingを除く、要素が入る領域幅の和(TODO: Content Inset考慮)
let fillWidth = count == 1 ? contentSize.width : contentSize.width - (spacing * (fCount - 1))
// 要素の大きさは端数を切り捨て
let baseItemWidth = (fillWidth / fCount).rounded(.down)
// 端数切り捨てによって生じる余り
let residue = Int(fillWidth - (baseItemWidth * fCount))
return (0..<count).reduce(into: [NSCollectionLayoutGroupCustomItem]()) { items, n in
// x, widthは常に整数
// 正確なItem Spacingを確保するため、「余り」は要素幅に加える
let x = n == 0 ? 0 : items[n - 1].frame.maxX + spacing
let width = baseItemWidth + (n < residue ? 1 : 0)
items.append(NSCollectionLayoutGroupCustomItem(frame: .init(
x: x,
y: 0,
width: width,
height: contentSize.height
)))
}
}
let section = self.init(group: group)
section.interGroupSpacing = spacing
return section
}
}
custom()
メソッドを用いる場合interItemSpacing
プロパティは無視されます。
比較
.horizontal()
でレイアウトした場合
各要素の位置と大きさに端数があるため、見かけ上のItem Spacingにバラつきがあります。
.custom()
でレイアウトした場合
各要素の位置と大きさに端数はなく、Item Spacingが均一です。
但し要素の大きさは帳尻合わせのため、すべて同じにならない場合があります。
その他気になったこと
.custom()
でレイアウトを組む際はセクションのContent Insetも考慮すべきですが、 NSCollectionLayoutEnvironment#contentInset
は常に 0
です...不具合なのかどうか分かりませんが、Content Insetがあるレイアウトの場合は注意が必要ですね。