[Swift] iOS14 から使える UICollectionView の CellRegistration を拡張する


# 初めに

iOS14 から `UICollectionView` に新しいい `API` が 追加されました。

https://developer.apple.com/videos/play/wwdc2020/10026

`Lists in UICollectionView` では `UITableView` ライクな UI の実装を紹介していますが、それに限らず便利なので使ってみました。

今回は実際に使用してみて、**若干不便だった**点もあったので、拡張してみたお話です。


# 実装

## 1. 今までの UICollectionView おける実装の課題

今までの`UICollectionView`を振り返ると

- `Delagate`、`DataSource`、`Layout`をそれぞれ設定
- `register` でセルの登録
- `dequeueReusableCell` で呼び出す

という仕様でした。

これには問題があり、セルの呼び出しが**Stringの直書き**で 
- `register`
- `dequeueReusableCell`

の２重管理になっているという点です。

スペルミスなどで読み込めずクラッシュしてしまい、**バグが起きやすい仕様**になっていました。`extension`での拡張やライブラリ(`R.Swift`など)で `String` の直書きは解決できましたが、それでも `register` の二重管理問題は残ります。

この問題を解決できるのが `CellRegistration`の魅力です。

## 2. CellRegistration を使っての実装

### （１） CellRegistrationの実装

`CellRegistration` を使った簡単な例を見てもらう方が早いでしょう。

```.swift
// セルの設定
let registration = UICollectionView.CellRegistration<UICollectionViewCell, String>() { cell, indexPath, name in
    // デフォルトで用意されたオプションを設定できる
    var config = UIListContentConfiguration.cell()
    config.text = name
    config.secondaryText = "text"
    // セルの設定をする
    cell.contentConfiguration = config
    cell.backgroundColor = .yellow
}
```

形式としては `CellRegistration<セルの型, モデルの型>` という形で設定します。今回はカスタムなセルではなく用意されたデフォルトのセルで行っています。

`UITableViewCell` にはデフォルトのセルがいくつか用意されていましたが、`UICollectionViewCell` にも用意され、`UIListContentConfiguration` でそれを設定することができます。

あとは、呼び出し側で作成した `CellRegistration` をセットするだけです。

```.swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: /*モデル*/)
}
```

### （２） 全体の実装

実際に動くコードの全体像を見てみましょう。ミニマムな実装例を用意しました。

```.swift
final class ViewController: UIViewController {

    /// ① `CellRegistration`でセルの設定をする
    private let registration = UICollectionView.CellRegistration<UICollectionViewCell, String>() { cell, indexPath, name in
        var config = UIListContentConfiguration.cell()
        config.text = name
        config.secondaryText = "text"
        cell.contentConfiguration = config
        cell.backgroundColor = .yellow
    }

    private lazy var collectionView: UICollectionView = {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: 100.0, height: 100.0)
        let collection = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collection.dataSource = self
        collection.backgroundColor = .white
        return collection
    }()

    private let models = ["apple", "orange", "banana", "melon", "lemon"]

    override func loadView() {
        self.view = collectionView
    }
}

// MARK: - UICollectionViewDataSource
extension ViewController: UICollectionViewDataSource {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        models.count
    }

     func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        /// ② `dequeueConfiguredReusableCell`で`CellRegistration`をセットする
        collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: models[indexPath.item])
    }
}
```

先程の（１）でも説明しましたが、実にシンプルで、
① `CellRegistration` でセルの設定をする
② `dequeueConfiguredReusableCell` で `CellRegistration` をセットする
だけになります。

また、実際の画面はこうなります。

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/175242/2f204c73-8f8d-e520-97a4-e81d49b1799b.png)



セルの設定とレイアウトの設定次第で、既存の `UICollectionView` のように**自由な実装**ができます。


## 3. カスタムセルを使う場合

カスタムセルを使う場合、コードが少しだけ長くなります。
また、こういった場合は複数のセクションもつため、**コードが肥大化しやすい**でしょう。

まずはカスタムセルの実装を見つつ、拡張するコードを紹介していきます。

### （１） クラスから生成するカスタムセル

以下のセル・モデルがあった時、

```AnimalModel.swift
struct AnimalModel {
    var name: String
}
```

```AnimalCell.swift
final class AnimalCell: UICollectionViewListCell {
    private let nameLabel = UILabel()
    /* 略 */
    func configure(with model: AnimalModel) {
        nameLabel.text = model.name
    }
}
```

`CellRegistration` は `<セルの型, モデルの型>` という形で生成します。

```.swift
let registration = UICollectionView.CellRegistration<AnimalCell, AnimalModel>() { cell, indexPath, model in
    cell.configure(with: model)
}
```

デフォルトのセルと生成方法はなんら変わりはありません。

### （２） Xibから生成するカスタムセル

以下のセル・Xib・モデルがあった時、

```SportModel.swift
struct SportModel {
    var name: String
}
```

```SportCell.swift
final class SportCell: UICollectionViewListCell {
    @IBOutlet private weak var nameLabel: UILabel!
    func configure(with model: SportModel) {
        nameLabel.text = model.name
    }
}
```

```SportCell.xib
/* xmlなので記載せず */
```


同じように`<セルの型, モデルの型>` という形で生成しますが、Xibを呼び出す必要があります。

```.swift
let registration = UICollectionView.CellRegistration<SportCell, SportModel>(
    cellNib: UINib(nibName: String(describing: SportCell.self), bundle: nil)
) { cell, indexPath, model in
    cell.configure(with: model)
}
```

コードが横に長くなってしまうので改行していますが、このように簡単に呼び出すことができます。

## 4. カスタムセルを拡張する


追記(2021/01)

 **拡張コードに関して、iOS15 ではクラッシュする可能性があります**

https://developer.apple.com/forums/thread/681739

**以下の拡張はそのままでは使用できないので、参考程度のコードだと思って御覧ください。** 


### （１） 既存コードの課題

カスタムセルを複数のセクションで実装すると、以下のようなコードになると思います。

- example

```.swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let section = Section(rawValue: indexPath.section) else { return .init() }

    switch section {
    case .animal:
        let registration = UICollectionView.CellRegistration<AnimalCell, AnimalModel>() { cell, indexPath, model in
            cell.configure(with: model)
        }
        return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: animals[indexPath.item])

    case .sport:
        let registration = UICollectionView.CellRegistration<SportCell, SportModel>(
            cellNib: UINib(nibName: String(describing: SportCell.self), bundle: nil)
        ) { cell, indexPath, model in
            cell.configure(with: model)
        }
        return collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: sports[indexPath.item])
    }
}
```

ぱっとみでも**とても見づらい**です。これは２つのセクションしかないですが、実際のプロダクションコードでは、多くのセルを使うことになるでしょう。そこで`extension`を拡張して**コードを隠蔽化**します。

### （２） UICollectionView の extension を拡張する 

拡張することで

- UICollectionView.CellRegistration
- dequeueConfiguredReusableCell

を隠蔽化することができ、コードを記述する際に意識しなくて済むようになります。

<br>

拡張には

- クラスから生成するカスタムセル
- Xibから生成するカスタムセル

の２つをそれぞれ拡張します。

#### ① クラスから生成するカスタムセル（Itemあり）

```.swift
extension UICollectionView {

    func dequeueReusableCell<C, I>(
        class: C.Type,
        with item: I,
        at indexPath: IndexPath,
        handler: @escaping UICollectionView.CellRegistration<C, I>.Handler
    ) -> UICollectionViewCell where
        C: UICollectionViewCell
    {
        dequeueConfiguredReusableCell(
            using: UICollectionView.CellRegistration<C, I>(
                handler: handler
            ),
            for: indexPath,
            item: item
        )
    }
}
```

#### ② Xibから生成するカスタムセル（Itemあり）

```.swift
extension UICollectionView {

    func dequeueReusableCell<C, I>(
        type: C.Type,
        with item: I,
        at indexPath: IndexPath,
        handler: @escaping UICollectionView.CellRegistration<C, I>.Handler
    ) -> UICollectionViewCell where
        C: UICollectionViewCell
    {
        dequeueConfiguredReusableCell(
            using: UICollectionView.CellRegistration<C, I>(
                cellNib: UINib(nibName: String(describing: type.self), bundle: nil),
                handler: handler
            ),
            for: indexPath,
            item: item
        )
    }
}
```

また、header や footer を作る際には、 `SupplementaryRegistration` というのがあるので、同じように拡張することができるはずです。

また、アイテム（モデル）なしでセルを生成したい場合もあるので、その場合も掲載しておきます。
（長くなるので、見たい人は**「▶︎」を開いてください**）

#### ③ クラスから生成するカスタムセル（Itemなし）

<details>
<summary>モデルを必要としないセルの拡張</summary>
<div>

```.swift
func dequeueReusableCell<C>(
    class: C.Type,
    at indexPath: IndexPath,
    handler: @escaping (C, IndexPath) -> Void = { _, _ in }
) -> UICollectionViewCell where
    C: UICollectionViewCell
{
    /// モデルがないのでStringを設定しておく
    dequeueConfiguredReusableCell(
        using: UICollectionView.CellRegistration<C, String>(
            handler: { c, i, _ in handler(c, i) }
        ),
        for: indexPath,
        item: ""
    )
}
```

</div>
</details>


#### ④ Xibから生成するカスタムセル（Itemなし）

<details>
<summary>モデルを必要としないセルの拡張</summary>
<div>

```.swift
func dequeueReusableCell<C>(
    type: C.Type,
    at indexPath: IndexPath,
    handler: @escaping (C, IndexPath) -> Void = { _, _ in }
) -> UICollectionViewCell where
    C: UICollectionViewCell
{
    /// モデルがないのでStringを設定しておく
    dequeueConfiguredReusableCell(
        using: UICollectionView.CellRegistration<C, String>(
            cellNib: UINib(nibName: String(describing: type.self), bundle: nil),
            handler: { c, i, _ in handler(c, i) }
        ),
        for: indexPath,
        item: ""
    )
}
```

</div>
</details>


　
### （３） 拡張コードを使用した例

先程の「（１）既存コードの課題」で出したサンプルを書き換えました。

- example

```.swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let section = Section(rawValue: indexPath.section) else { return .init() }

    switch section {
    case .animal:
        // Class から セルを生成
        return collectionView.dequeueReusableCell(class: AnimalCell.self,
                                                  with: animals[indexPath.row],
                                                  at: indexPath) { cell, _, item in
            cell.configure(with: item)
        }

    case .sport:
        // Xib から セルを生成
        return collectionView.dequeueReusableCell(type: SportCell.self,
                                                  with: sports[indexPath.row],
                                                  at: indexPath) { cell, _, item in
            cell.configure(with: item)
        }
    }
}
```

**書き方が統一**され書きやすくなったかと思います。

# 終わりに

冒頭にも書きましたが、`CellRegistration` をつかうことで 

- String のハードコード
- ２重管理

が無くなるのは大きいなと感じました。

SwiftUIが台頭してきたので今更感もありつつ、まだまだ UIKit は現役だと思うので、積極的に使っていきたいですね！

## 参考

サンプルを Github にあげておきましたのでぜひご覧くださいmm

https://github.com/crane-hiromu/UICollectionView_CellRegistration_Sample