CollectionViewにて複数のアイテムを選択する

# 初めに
この記事では、`UICollectionView`にて複数アイテムを選択する方法を紹介します!

全体のソースコードはこちらです🔨
https://github.com/h-taro/CollectionViewSample

完成形はこちらです✌️
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551042/bbdcdfe8-ebbd-136d-59d9-c39697aca9f9.gif" width="300" />

# 環境
`Xcode 12.5.1`
`Swift 5.4.2`
`macOS Big Sur 11.5.2`

# 実装方法
大まかな実装方法は以下です！
1. `UICollectionView`で複数選択できるように設定する
2. 選択されたアイテムを保存する配列を準備する。(今回の実装の場合`selectedNumbers`)
3. アイテムがタップされた時に`selectedNumbers`へ追加する
4. 選択されたアイテムがもう一度タップされた場合、`selectedNumbers`から削除する

該当箇所のコードはこちらです！🚀

```swift:ViewController.swift

import UIKit

class ViewController: UIViewController {
  ...
  override func viewDidLoad() {
    super.viewDidLoad()
    
    ...

    // CollectionViewで複数選択できるように設定する
    collectionView.allowsMultipleSelection = true
  }
}

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

  // cell初期化時に選択されているか評価する。
  // 選択されている場合は、ハイライトする。そうでない場合は、デフォルトのスタイルを当てる。
  func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
    let number = numbers[indexPath.row]
    cell.setupLabel(text: String(number))
    if isSelected(number: number) {
      cell.changeBorder()
    } else {
      cell.initBorder()
    }
    return cell
  }
}

// MARK:- UICollectionViewDelegate
extension ViewController: UICollectionViewDelegate {
  // 選択された時にselectedNumbersにアイテムを追加する
  // 選択された時のスタイルを当てる
  func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    guard
      let cell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell
    else { return }
    addSelectedNumber(number: numbers[indexPath.row])
    DispatchQueue.main.async {
      cell.changeBorder()
    }
  }
  
  // 選択解除時された時にselectedNumbersからアイテムを削除する
  // スタイルを元に戻す
  func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    guard
      let cell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell
    else { return }
    removeSelectedNumber(number: numbers[indexPath.row])
    DispatchQueue.main.async {
      cell.initBorder()
    }
  }
}

extension ViewController {
  ...
  
  private func addSelectedNumber(number: Int) {
    selectedNumbers.append(number)
  }
  
  private func removeSelectedNumber(number: Int) {
    selectedNumbers = selectedNumbers.filter({ selectedNumber in
      return selectedNumber != number
    })
  }
  
  private func isSelected(number: Int) -> Bool {
    return selectedNumbers.contains(number)
  }
}
```

# 注意点
選択状況の制御について注意点があります。
`CollectionView`ではセルが再利用されます。
そのため、スクロールした時に選択状況まで再利用され、
よく分からない状態になってしまいます。
なので、今回の実装ではセルが初期化されるたびに選択状況を評価し、
結果に応じてスタイルを変更するようにしました！

```swift:ViewController.swift
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
  let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
  let number = numbers[indexPath.row]

  // 選択されているなら、ハイライト。そうでないなら、デフォルトのスタイルを当てる。
  cell.setupLabel(text: String(number))
  if isSelected(number: number) {
    cell.changeBorder()
  } else {
    cell.initBorder()
  }
  return cell
}
```

# 最後に
今回のような`CollectionView`にて複数アイテムを選択したい時は結構あると思います。
しかし、実際に実装してみると思い通りにならない箇所が多々ありました。
特に、セルが再利用されるときに選択状況まで再利用される箇所ははまりどころでした。
参考にしていただけると嬉しいです！🙏