UICollectionViewCompositionalLayoutを使ってみた

#はじめに
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を生成するクラスを用意


<img width="200" alt="qiita-image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/254298/017e6229-041b-811e-f6b7-374cab447ed5.png">

###例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)
        // １つの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