UICollectionViewで同じサイズのセルを綺麗に並べる (Adaptive Layout)

全て同じサイズの写真を美しくグリッド表示するために試して良かった方法をご紹介します。

目次

ちなみにセルのサイズがバラバラな場合

Self-Sizing

セルにラベルが含まれていて文字数に応じて高さが異なる場合などに利用します。ContentViewをセルにフィットさせる制約を予めコードで設定しておきます。

class CollectionViewCell: UICollectionViewCell {

    override func awakeFromNib() {
        super.awakeFromNib()
        contentView.translatesAutoresizingMaskIntoConstraints = false
        contentView.topAnchor.constraint(equalTo: topAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
        contentView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
        contentView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
    }
}

UICollectionViewDelegateFlowLayoutのsizeForItemAtでサイズ指定

画像のアスペクト比に応じたサイズにしたり、データソースに基づいたサイズを個別に設定できます。

UICollectionViewLayoutをサブクラス化

個別にセル間隔を変えたり、順番を変えたり、高度なレイアウトを実現できます。

CollectionViewがTableViewより大変な理由

サイズの変化に対して中のセルサイズが自動的にリサイズされないので、そのタイミング計算方法を指定してあげる必要があります。

セルをリサイズするタイミング

// 端末回転時
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)

// Size Class変更時
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?)

// サイズ変更時など
override func viewDidLayoutSubviews()

これらのタイミングでitemSizeを設定すればセルのサイズを変更できます。

(collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize = CGSize(width: 100, height: 100)

ただし、viewDidLayoutSubviews()に関しては問題があります。

  • containerViewとして使うVCの場合に無限ループ発生(iOSのバージョンや状況によるかも)
  • ドラッグ&ドロップの並べ替えの時にも呼ばれてしまい、ドロップアニメーションが残像のように2重になる

layoutSubviews系は頻繁に呼ばれるので注意が必要ですが、この2つに該当しなければ使っても大丈夫そうです。

UICollectionViewFlowLayoutにセルのリサイズを担当させる

invalidateLayout()のたびにprepareメソッドが呼ばれるので、この中でitemSizeを更新すれば綺麗にレイアウトできます。

例えば以下のようなサブクラスを使うと、横幅がレギュラーサイズなら8列コンパクトサイズなら4列に変形できます。

class TilesCollectionViewFlowLayout: UICollectionViewFlowLayout {

    required init?(coder: NSCoder) {
        super.init(coder: coder)
    }
    
    @IBInspectable var numberOfColumns: Int = 8 {
        didSet {
            invalidateLayout()
        }
    }
    
    @IBInspectable var heightRatio: CGFloat = 1 {
        didSet {
            invalidateLayout()
        }
    }
    
    private func updateItemSize() {
        guard let collectionView = collectionView else {
            return
        }
        let isWide = collectionView.traitCollection.horizontalSizeClass != .compact || collectionView.traitCollection.verticalSizeClass == .compact
        let columns = CGFloat(isWide ? numberOfColumns : numberOfColumns / 2)
        let margins = collectionView.safeAreaInsets.left + collectionView.safeAreaInsets.right + sectionInset.left + sectionInset.right
        let spacings = minimumLineSpacing * (columns - 1)
        let width = (collectionView.bounds.width - margins - spacings) / columns
        let height = width * heightRatio
        itemSize = CGSize(width: width, height: height)
    }
    
    override func prepare() {
        super.prepare()
        updateItemSize()
    }
}

こうしておくと、端末の回転だけでなく、iPadのSplit View、Slide Over、マルチウィンドウを使う場合でも美しくレイアウトされます。

レイアウトを交換する

レイアウトを一変させたい場合は、setCollectionViewLayout(:animated:completion:)を使うと、アニメーション付きで別のレイアウトに交換できます。

CollectionViewのサイズをコンテンツにフィットさせる

class CollectionView: UICollectionView {
    override var intrinsicContentSize: CGSize {
        return contentSize
    }
}

セルサイズ確定後に、invalidateIntrinsicContentSize()