UICollectionViewのカスタムセルの作り方とセルの再利用について

UICollectionViewのカスタムセルの作り方をご紹介します。
また、UICollectionViewの効率的な処理を支えるセルの再利用について、その仕組みおよびカスタムセル実装時の注意点についても触れます。

## カスタムセルを利用したUICollectionViewの実装
最終的に作成する画面は以下のようなものです。
UILabelを縦に2つ並べただけのセルです。
後半のセルの再利用の仕組みについて説明するのに都合の良い画面になってます:sweat_smile:

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/3d566cf6-9622-5c0d-e072-46fa54ee124e.png)

### StoryBoardの設定
まずはStoryBoardから。
ViewControllerにUICollectionViewを配置します。
UICollectionViewには上下左右にSafeAreaに対する制約をつけています。
UICollectionView内のセルはここでは削除しておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/87d7c38d-5a6d-0232-c357-07a36dc5bbea.png)

### カスタムセル用のXIBファイルの作成
次はカスタムセルのxibファイルです。
XCodeメニューのFile > New > File...とたどり、Cocoa Touch Classを選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/fd09377e-c5f2-8f72-7779-1fc027a9b802.png)

クラスの型をUICollectionViewCellにして、名前をつけます(ここでは`CustomCell`という名前にします)。
Also create XIB fileにチェックを付けておいてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/f64f65d3-d37a-495e-f2ab-63a185b3cd3a.png)

CustomCell.xibというファイルが作成されたら、適当にUILabelを2つ配置した以下のような画面を作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/6a650af9-23cf-739a-1a13-42725af517ff.png)

### ViewControllerの実装
続いてViewControllerの実装です。

```swift
collectionView.register(UINib(nibName: "CustomCell", bundle: nil), forCellWithReuseIdentifier: "CustomCell")
```

まず、コレクションビューで使用するセルを`register(_:forCellWithReuseIdentifier:)`メソッドで登録します。
第一引数にはUINibオブジェクトを渡します。
UINibのイニシャライザに渡している`CustomCell`という文字列は、セルのXIBファイル名と一致します。
第二引数の`forCellWithReuseIdentifier`には、セルの再利用識別子というものを指定します。
ここの文字列は任意ですが、ここでは`CustomCell`を指定します。

続いてデータソースの実装です。
ViewControllerにUICollectionViewDataSourceプロトコルを実装します。

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

`collectionView(_:numberOfItemsInSection:)`メソッドでは、コレクションビューに配置するセルの個数を返すようにします。
`models`はセルに表示するデータを持つモデルオブジェクトの配列です。
モデルオブジェクトについてはあとで触れます。

```swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath)

    if let cell = cell as? CustomCell {
        cell.setupCell(model: models[indexPath.row])
    }

    return cell
}
```

`collectionView(_:cellForItemAt:)`メソッドでは、セルの設定を行います。
`dequeueReusableCell(withReuseIdentifier:for:)`メソッドでセルオブジェクトを取り出し、セルにモデルオブジェクトを渡して設定を行っています。
新しくCustomCellオブジェクトを生成するのではなく、`dequeueReusableCell(withReuseIdentifier:for:)`メソッドでキューに溜まっているセルオブジェクトを再利用するというところがポイントです。
あとで詳しく触れます。


全体のViewControllerの実装は以下のとおりです。

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController {
    @IBOutlet weak var collectionView: UICollectionView!

    let models = Model.createModels()

    override func viewDidLoad() {
        super.viewDidLoad()

        collectionView.dataSource = self

        collectionView.register(UINib(nibName: "CustomCell", bundle: nil), forCellWithReuseIdentifier: "CustomCell")

        // セルの大きさを設定
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = CGSize(width: collectionView.frame.width, height: 100)
        collectionView.collectionViewLayout = layout
    }
}

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

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CustomCell", for: indexPath)

        if let cell = cell as? CustomCell {
            cell.setupCell(model: models[indexPath.row])
        }

        return cell
    }
}
```

### カスタムセルクラスの実装
カスタムセルのクラスの実装は以下のとおりです。

```swift:CustomCell.swift
import UIKit

class CustomCell: UICollectionViewCell {
    @IBOutlet weak var title: UILabel!
    @IBOutlet weak var subTitle: UILabel!

    func setupCell(model: Model) {
        title.text = model.title

        if let text = model.subTitle {
            subTitle.text = text
        }

        self.backgroundColor = .lightGray
    }
}
```

`title`と`subTitle`はXIBファイル上のUILabelオブジェクトとリンクしています。
`setupCell(model:)`メソッドはモデルオブジェクトを受け取って、2つのラベルの設定を行います。

### モデルクラスの実装
最後にモデルのクラスです。

```swift:Model.swift
struct Model {
    let title: String
    let subTitle: String?

    static func createModels() -> [Model] {
        return [
            Model(title: "AAAAAAAAAA", subTitle: "aaaaaaaaaa"),
            Model(title: "BBBBBBBBBB", subTitle: "bbbbbbbbbb"),
            Model(title: "CCCCCCCCCC", subTitle: "cccccccccc"),
            Model(title: "DDDDDDDDDD", subTitle: "dddddddddd"),
            Model(title: "EEEEEEEEEE", subTitle: "eeeeeeeeee"),
            Model(title: "FFFFFFFFFF", subTitle: "ffffffffff"),
            Model(title: "GGGGGGGGGG", subTitle: "gggggggggg"),
            Model(title: "HHHHHHHHHH", subTitle: "hhhhhhhhhh"),
            Model(title: "IIIIIIIIII", subTitle: "iiiiiiiiii"),
            Model(title: "JJJJJJJJJJ", subTitle: "jjjjjjjjjj"),
            Model(title: "KKKKKKKKKK", subTitle: "kkkkkkkkkk"),
            Model(title: "LLLLLLLLLL", subTitle: "llllllllll"),
            Model(title: "MMMMMMMMMM", subTitle: "mmmmmmmmmm"),
            Model(title: "NNNNNNNNNN", subTitle: nil),
            Model(title: "OOOOOOOOOO", subTitle: nil),
            Model(title: "PPPPPPPPPP", subTitle: nil),
            Model(title: "QQQQQQQQQQ", subTitle: nil),
            Model(title: "RRRRRRRRRR", subTitle: nil),
            Model(title: "SSSSSSSSSS", subTitle: nil),
            Model(title: "TTTTTTTTTT", subTitle: nil),
            Model(title: "UUUUUUUUUU", subTitle: nil),
            Model(title: "VVVVVVVVVV", subTitle: nil),
            Model(title: "WWWWWWWWWW", subTitle: nil),
            Model(title: "XXXXXXXXXX", subTitle: nil),
            Model(title: "YYYYYYYYYY", subTitle: nil),
            Model(title: "ZZZZZZZZZZ", subTitle: nil),
        ]
    }
}
```

モデルは`title`と`subTitle`という2つのプロパティを持っています。
`subTitle`はオプショナル型なので、nilの可能性もあります。
`createModels()`はコレクションビューに表示するデータを作成します。
ViewControllerはこのメソッドで作成したデータを`models`プロパティに保持しています。

これらの実装でアプリを起動すると、以下のような画面になるはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/3d566cf6-9622-5c0d-e072-46fa54ee124e.png)

## セルの再利用の仕組みについて
上で紹介したアプリはかなり作為的な実装になってますが、セルの再利用の仕組みについて説明するのに都合が良いのです。

### 上記アプリの問題点
画面を下にスクロールしていくと、ある時点からおかしなことが起こります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/0bf831ff-430a-f0dc-20d0-ece633d011f5.png)

`NNNNNNNNNN`というタイトルの下に、`aaaaaaaaaa`というサブタイトルが表示されています。
ViewControllerの`models`には、`Model(title: "NNNNNNNNNN", subTitle: nil)`というデータが入っているので、サブタイトルには何も表示されないのが期待している動作です。

### セルは再利用されている
画面を下にスクロールしていくと、上にあるセルは表示されなくなり、下から新しいセルが表示されます。
この時、コレクションビューは新しいセルオブジェクトを作成するのではなく、表示されなくなったセルを内部的なキューに入れておき、新しいセルが表示されるときにキューからセルを取り出すという方法を取ります。

記事の最初の方で触れた`dequeueReusableCell(withReuseIdentifier:for:)`というメソッドは、キューに溜まっている使われなくなったセルを取り出すためのものなのです。
キューはセルの種類ごとに用意されていて、再利用識別子を使ってどのキューからセルを取り出すかを決定します。

`NNNNNNNNNN`というタイトルの下に`aaaaaaaaaa`というサブタイトルが表示されていたのは、最初に`AAAAAAAAAA`というタイトルを表示していたセルを再利用したからです。

### セルは初期化してから使用する
セルを使用するときに毎回初期化を行うことで、このようは問題はなくなります。
UICollectionViewCellには`prepareForReuse()`というメソッドが用意されていて、これはセルがキューから取り出される際に呼ばれます。
以下のように`title`と`subTitle`に空文字列をセットしてあげることで、期待した動作になります。

```swift
override func prepareForReuse() {
    super.prepareForReuse()

    title.text = ""
    subTitle.text = ""
}
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/81715/82138636-0736-c359-aa9d-e2886810d42d.png)