1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

UICollectionViewCompositionalLayoutを使ってみた

Last updated at Posted at 2020-02-03

はじめに

UICollectionViewLayoutに関して、iOS13からUICollectionViewCompositionalLayoutという新しいレイアウト方法が追加されたため、iOS12以前で使用していたレイアウト方法と比較してみました。

主な違い

  • UICollectionViewLayoutの生成方法
  • UICollectionViewのDataSourceの管理

※ UICollectionViewCompositionalLayout自体の仕組みについては、[こちら](時代の変化に応じて進化するCollectionView ~Compositional LayoutsとDiffable Data Sources~)の記事が非常にわかりやすかったです。

UICollectionViewLayoutの生成方法

iOS12以前では、UICollectionViewLayoutやUICollectionViewFlowLayoutのサブクラスを独自に用意したり、UICollectionViewDelegateFlowLayoutを利用したりしてレイアウトを生成していました。
その中に、iOS13からUICollectionViewCompositionalLayoutが加わったイメージです。

以下、3つの方法でグリッド形式(3×n)のレイアウトを作ってみています。

  1. UICollectionViewFlowLayoutのサブクラスを用意
  2. UICollectionViewLayoutのサブクラスを用意
  3. UICollectionViewCompositionalLayoutを生成するクラスを用意
qiita-image

例1: UICollectionViewFlowLayoutのサブクラスを用意(iOS12以前)

※ iOS13以降でも使用できます。

GridLayout.swift
class GridLayout: UICollectionViewFlowLayout {
    
    let itemCount = CGFloat(3)
    let itemSpacing = CGFloat(1)
    
    override func prepare() {
        super.prepare()
        itemSize = itemSize()
        minimumLineSpacing = itemSpacing
        minimumInteritemSpacing = itemSpacing
        sectionInsetReference = .fromSafeArea
    }
    
    private func itemSize() -> CGSize {
        guard let cv = collectionView else { return CGSize.zero }
        let itemLength = cv.bounds.width / itemCount - itemSpacing
        return CGSize(width: itemLength, height: itemLength)
    }
    
}
// レイアウトの適用
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: GridLayout())

例2: UICollectionViewLayoutのサブクラスを用意(iOS12以前)

※ iOS13以降でも使用できます。

GridLayout.swift

/// グリッド形式のレイアウトを生成するクラス
class GridLayout: UICollectionViewLayout {

    // MARK: - Private Propaty

    private var cachedAttributes = [UICollectionViewLayoutAttributes]()
    // レイアウトの総Height
    private var contentHeight: CGFloat = 0
    // レイアウトの総Width
    private var contentWidth: CGFloat {
        guard let collectionView = collectionView else { return 0 }
        let insets = collectionView.contentInset
        return collectionView.bounds.width - (insets.left + insets.right)
    }

    // MARK: - Life Cycle

    override func prepare() {
        guard cachedAttributes.isEmpty, let collectionView = collectionView else { return }

        let numberOfColumns = 3
        let lineCount = numberOfColumns - 1
        let itemSpacing = CGFloat(1)
        let cellPadding = CGFloat.zero
        let cellLength = (contentWidth - (itemSpacing * CGFloat(lineCount))) / CGFloat(numberOfColumns)
        let cellXOffsets = (0 ..< numberOfColumns).map { CGFloat($0) * (cellLength + 1) }
        var cellYOffsets = [CGFloat](repeating: 0, count: numberOfColumns)
        var currentColumnNumber = 0

        (0 ..< collectionView.numberOfItems(inSection: 0)).forEach {
            let indexPath = IndexPath(item: $0, section: 0)
            let cellFrame = CGRect(x: cellXOffsets[currentColumnNumber],
                                   y: cellYOffsets[currentColumnNumber],
                                   width: cellLength,
                                   height: cellLength)
            cellYOffsets[currentColumnNumber] = cellYOffsets[currentColumnNumber] + (cellLength + itemSpacing)
            currentColumnNumber = currentColumnNumber < (numberOfColumns - 1) ? currentColumnNumber + 1 : 0

            let itemFrame = cellFrame.insetBy(dx: cellPadding, dy: cellPadding)
            // Attributesを追加
            let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
            attributes.frame = itemFrame
            cachedAttributes.append(attributes)
            // ContentSizeを更新
            contentHeight = max(contentHeight, cellFrame.maxY)
        }
    }

    override var collectionViewContentSize: CGSize {
        // prepareが終わった後に呼ばれるので、計算したcontentHeightを返す
        return CGSize(width: contentWidth, height: contentHeight)
    }

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        return cachedAttributes.filter({ (layoutAttributes) -> Bool in
            rect.intersects(layoutAttributes.frame)
        })
    }

    override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
        return cachedAttributes[indexPath.item]
    }
}
// レイアウトの適用
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: GridLayout())

例3: UICollectionViewCompositionalLayoutを生成するクラスを用意(iOS13以降)

PhotoListLayout.swift
    /// グリッド形式のレイアウトを生成する
    /// - Parameters:
    ///   - collectionViewBounds: UICollectionViewのBounds
    ///   - itemCount: 1列に表示するitemの個数
    func gridLayout(collectionViewBounds: CGRect, itemCount: Int) -> UICollectionViewLayout {
        let lineCount = itemCount - 1
        let itemSpacing = CGFloat(1) // セル間のスペース
        let itemLength = (collectionViewBounds.width - (itemSpacing * CGFloat(lineCount))) / CGFloat(itemCount)
        // 1つのitemを生成
        // .absoluteは固定値で指定する方法
        let item = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(widthDimension: .absolute(itemLength),
                                                                             heightDimension: .absolute(itemLength)))
        // itemを3つ横並びにしたGroupを生成
        // .fractional~は親Viewとの割合
        let items = NSCollectionLayoutGroup.horizontal(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                                                          heightDimension: .fractionalHeight(1.0)),
                                                       subitem: item,
                                                       count: itemCount)
        // Group内のitem間のスペースを設定
        items.interItemSpacing = .fixed(itemSpacing)

        // 生成したGroup(items)が縦に並んでいくGroupを生成(実質これがSection)
        let groups = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                                                         heightDimension: .absolute(itemLength)),
                                                      subitems: [items])
        // 用意したGroupを基にSectionを生成
        let section = NSCollectionLayoutSection(group: groups)

        // Section間のスペースを設定
        section.interGroupSpacing = itemSpacing

        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }
// レイアウトの適用
CollectionView = UICollectionView(frame: view.bounds, collectionViewLayout: PhotoListLayout.gridLayout(collectionViewBounds: view.bounds, itemCount: 3))

UICollectionViewのDataSourceの管理

iOS13からUICollectionViewDiffableDataSourceというUICollectionViewDataSourceを継承したクラスが登場し、このクラス内で表示するデータを管理できるようになりました。

これもコードベースで比較してみます。iOS12以前では、UICollectionViewDataSourceを直接クラスに準拠させる方法でデータを管理しています。そしてiOS13では、UICollectionViewDiffableDataSourceを使ってデータ管理をおこなってみます。

UICollectionViewDataSourceをControllerクラスに準拠させる(iOS12以前)

ViewController.swift
var collectionView: UICollectionView!
var photoList = [PHAsset]()
collectionView.dataSource = self

---------------------------------------------------------------

extension PhotoListViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return photoList.count // 配列等で持っているデータの数
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: PhotoListCell.identifier, for: indexPath) as! PhotoListCell
        cell.setImage(asset: photoList[indexPath.item]) // 配列等で持っているデータをIndexPathで指定して取得
        return cell
    }
}

UICollectionViewDiffableDataSourceでデータソースを管理(iOS13以降)

ViewController.swift
var collectionView: UICollectionView!
var dataSource: UICollectionViewDiffableDataSource<Int, PHAsset>!
var photoList = [PHAsset]()

---------------------------------------------------------------

    dataSource = UICollectionViewDiffableDataSource<Int, PHAsset>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, asset) -> UICollectionViewCell? in
            // セル生成処理
            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoListCell", for: indexPath) as! PhotoListCell
            cell.setImage(asset: asset)
            return cell
        })
        // 用意したリソースの反映
    var resource = NSDiffableDataSourceSnapshot<Int, PHAsset>()
    resource.appendSections([0]) // セクション数をセット
    resource.appendItems(photoList) // アイテムをセット
  // 注意点: 複数セクションを使用する場合は、セクション毎にアイテムのセットも行う 
    dataSource.apply(resource, animatingDifferences: true)

ざっくり言うと、iOS12以前までUICollectionViewDataSourceで手続き的にやっていたことが、iOS13からは宣言的に書けるようになったように思いました。

以上

サンプル

今回作成したサンプルは、以下のリポジトリにあります。
※ サンプルはiOS13用のみです。
https://github.com/ddd503/UICollectionViewCompositionalLayout-UICollectionViewDiffableDataSource-Sample

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?