anz blog

UICollectionViewFlowLayoutが予期せぬ配置してたのでちょっと深堀りしてみた

2018-04-04 #Swift

UICollectionViewって各Cellのサイズ調整が簡単にできて結構好きなのですが、
ちょっと「おぉ...こんな挙動するのか」っていうケースがあったのでそのメモ。

UICollectionView...Cell配置のアルゴリズムよくわからん...

です(笑)

環境

  • Xcode v9.2

やりたいこと

今回の話は、レイアウト次第な話な気がするので...
まずはどんなレイアウトにしようとしたかっていう説明から。

1件目のCellは画面幅いっぱいにして、ソレ以降は2カラムで配置していく...
っていうそこまでトリッキーじゃない、むしろ王道みたいなレイアウトです。

collectionview_example01-1

こういう感じです。

再現コード

class ViewController: UIViewController {
    
    @IBOutlet weak var collectionView: UICollectionView!
    
    private let items: [UIColor] = [
        .blue,
        .red,
        .yellow,
        .green
    ]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "CELL")
        self.collectionView.dataSource = self
        self.collectionView.delegate = self
    }
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.items.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL", for: indexPath)
        cell.contentView.backgroundColor = self.items[indexPath.row]
        return cell
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let width = collectionView.bounds.width
        let half = width * 0.5
        if indexPath.row == 0 {
            return CGSize(width: width, height: half)
        } else {
            return CGSize(width: half, height: half)
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        return .zero
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
        return .leastNormalMagnitude
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return .leastNormalMagnitude
    }
}

至って普通だと思います(たぶん)
これで、目指したいレイアウトは実現できているのですが...
とあるケースのときだけ、ちょっと「お?...おぅ」みたいな感じになります。

意図していないレイアウト

表示する件数を2件にします。
items を下記のように変更するだけ

private let items: [UIColor] = [
    .blue,
    .red
]

これでやってみると...

collectionview_example02-1

...まさかのセンタリング!
Cellの配置は、Sizeそのまま入るならminSpacingを考慮した上でそのまま配置、入り切らない場合は、spacingで埋めるように配置(UIToolbarなんかと近いイメージ)とか思っていて、
この場合だと赤色は左詰めになるかなぁ〜とかっておもっていたのだけれど。
spaceingが均等に割り当たるからかな...と考えていると

collectionview_example04-1

blueとgreenで違うし...
spacingがどういう感じで埋められるのかイマイチわからない...🤔

とりあえず...CellをTopLeftで配置したいならまかせっきりっじゃダメだということですね!

ちなみに、表示する件数を1つにして、常に2カラム表示とした場合はどうなるのかというと...

collectionview_example03

まぁ...こうですよね。。
Cell幅を width * 0.8 ぐらいで一律表示すると、全部センタリングになるのですけどね...

対策

UICollectionViewFlowLayoutのサブクラスを作って、
layoutAttributesForElements(in:)とかlayoutAttributesForItem(at:)とかをを override して自分で位置調整をするとちゃんとできます。 今回の例で対応するとしたら、それぞれでIndexPathが参照できるので、

if indexPath.row == 0 || indexPath.row % 2 == 1 {
    frame.x = 0
} else {
    frame.x = size.width
}

こういう感じでしょうか(いろいろ端折ってますが)

まぁ・・・今回の実現したいレイアウトの場合だと、表示するアイテムが2件になったときだけ発現するものなので、目を瞑るっていうのも有りかと思います...(雑な 笑)