LoginSignup
2
1

More than 3 years have passed since last update.

UICollectionViewCompositionalLayoutで正確なItem Spacingを確保する

Last updated at Posted at 2020-06-02

以前 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 を独自計算する方法を採ります。

NSCollectionLayoutSection+Extension.swift
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にバラつきがあります。

default.jpg

.custom() でレイアウトした場合

各要素の位置と大きさに端数はなく、Item Spacingが均一です。
但し要素の大きさは帳尻合わせのため、すべて同じにならない場合があります。

spacing.jpg

その他気になったこと

.custom() でレイアウトを組む際はセクションのContent Insetも考慮すべきですが、 NSCollectionLayoutEnvironment#contentInset は常に 0 です...不具合なのかどうか分かりませんが、Content Insetがあるレイアウトの場合は注意が必要ですね。

2
1
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
2
1