1つのTableViewに単一選択と複数選択のセクションを用意する

# 環境
- Xcode11.3
- Swift5.1.3

# 経緯
とあるTableViewでセクションによって単一選択か複数選択かを分岐したい時ってありますよね。そうですよね、ええ、はい、あんまりないですよね。最近、初めて出会いました。
経験浅なので、セクション毎の設定とかできるんでしょうくらいに思っていたら、そんなものはなく、中々苦労したので今回取り上げました。

# ひとまず完成GIF 
銭湯セクションのみ単一選択で、他のセクションは複数選択で設定しています。
銭湯の選出については適当です(おすすめがあればコメントください)。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/66736918-36da-58f9-474d-218c0e2f7ae4.gif" width=320>

# 実装方法
今回の表題に関係のない箇所は省きます。
一応Repositoryのリンクは載せておきます。
[SingleAndMultipleSelectionTableView](https://github.com/james26k/SingleAndMultipleSelectionTableView)

### TableViewの設定
必要なのはこれだけです。
defaultではfalseになっているのでtrueにしてあげます。


```SingleAndMultipleSelectionTableViewController.swift
override func viewDidLoad() {
    super.viewDidLoad()
    // UITableView全体は複数選択可能に設定
    tableView.allowsMultipleSelection = true
}
```

### UITableViewDataSourceとUITableViewCell
#### UITableViewDataSource
ここで出てくる`cellTitles`はViewControllerのメンバ変数で各Cellのタイトルを格納している多次元配列です。[該当箇所はこちら](https://github.com/james26k/SingleAndMultipleSelectionTableView/blob/fc6aef7fb05ef970d6fbbc3c766dc8ba174731a2/SingleAndMultipleSelectionTableView/ViewModule/SingleAndMultipleSelectionTableViewController.swift#L16 "該当箇所はこちら")
withIdentifierに渡している`CustomCell`についてはこの後に説明します。

```SingleAndMultipleSelectionTableViewController.swift
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CustomCell", for: indexPath)
    cell.textLabel?.text = cellTitles[indexPath.section][indexPath.row]
    cell.selectionStyle = .none
    return cell
}
```

#### UITableViewCell
先ほど出てきたCustomCellです。
自前で`setSelected(_:animated:))`を記述したカスタムCellを用意すると、Cellの生成時orタップ時、よしなに`accessoryType`を管理してくれます。
初めはDelegateメソッドの`tableView(_:didSelectRowAt:)`と`tableView(_:didDeselectRowAt:)`に各々処理を書いていましたが、Cellの生成(かつリユース)時の処理も考えるとコード量も増えてしまいます。
これならコードスッキリトテモイイネ。

```SingleAndMultipleSelectionTableViewController.swift
private final class CustomCell: UITableViewCell {
    // Cellが生成されるタイミングでselectedの状態がtrueならチェックを付ける
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
        accessoryType = selected ? .checkmark : .none
    }
}
```

ここまでの実装でのGIFはこちら
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/476381/1d378a62-1146-6265-a529-eccc2b4df135.gif" width=320>

....あれ、なんか違う...

### UITableViewDelegate
これですね。Delegate。本命はここ、ここを実装しない限り表題の実現は叶いません。
コメントアウトにある程度説明を書いてしまっていますが、、
`tableView(_:willSelectRowAt)`で、Cellが選択されようとしている時に、単一セクション内のCellであれば排他制御処理を行います。
`tableView(_:willDeselectRowAt)`も同様です。


```SingleAndMultipleSelectionTableViewController.swift
extension SingleAndMultipleSelectionTableViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        // SingleSelectionSectionであれば処理を通す
        guard sections[indexPath.section] == "お好きな銭湯" else { return indexPath }
        // tableViewから選択されているIndexPathの配列を取得
        guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else { return indexPath }
        selectedIndexPaths.filter {
            // 選択したCell以外で既に選択されているIndexPathに絞る
            sections[$0.section] == "お好きな銭湯" && $0 != indexPath
        }.forEach {
            // 非選択状態にする
            tableView.deselectRow(at: $0, animated: true)
        }
        return indexPath
    }

    func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
        // 既に選択済みのCellであればnilを返して非選択処理を弾く
        return sections[indexPath.section] == "お好きな銭湯" ? nil : indexPath
    }
}
```

#### tableView(_:willSelectRowAt)

まず、どのセクションが単一選択か、そうではないのかを決めるguard文を記述します。
複数選択にしたいセクション内のindexPathが流れてきたらそのまま返します。

```SingleAndMultipleSelectionTableViewController.swift
// SingleSelectionSectionであれば処理を通す
guard sections[indexPath.section] == "お好きな銭湯" else { return indexPath }

```

お次はこちら

```SingleAndMultipleSelectionTableViewController.swift
// tableViewから選択されているIndexPathの配列を取得
guard let selectedIndexPaths = tableView.indexPathsForSelectedRows else { return indexPath }
selectedIndexPaths.filter {
    // 選択したCell以外で既に選択されているIndexPathに絞る
    sections[$0.section] == "お好きな銭湯" && $0 != indexPath
}.forEach {
    // 非選択状態にする
    tableView.deselectRow(at: $0, animated: true)
}
```
`tableView.indexPathsForSelectedRows`で、tableViewから選択されているIndexPathを取得します。この時取得できるindexPath郡はArray型`[IndexPath]`で返ってきます。

そして取得したindexPath達を次のようにしていきます。 (語彙力:pray_tone2:)

```SingleAndMultipleSelectionTableViewController.swift
.filter {
    // 選択したCell以外で既に選択されているIndexPathに絞る
    sections[$0.section] == "お好きな銭湯" && $0 != indexPath
}
```
単一セクション内で選択されようとしているindexPath **以外**のindexPathを抽出します。(既に単一セクション内で選択されていて、選択されようとしているindexPathとは異なるindexPathのことです)

そして、 (思考力:pray_tone2:)

```SingleAndMultipleSelectionTableViewController.swift
forEach {
    // 非選択状態にする
    tableView.deselectRow(at: $0, animated: true)
}
```
先ほど抽出したindexPath郡を全て非選択状態にします。

上記の一連の処理で、単一セクション内のindexPathに該当するCellが選択されようとした時のみ、
そのセクション内で他のindexPathに該当するCellが非選択状態に切り替わるような排他制御が実現されます。

#### tableView(_:willDeselectRowAt)

```SingleAndMultipleSelectionTableViewController.swift
func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
    // 既に選択済みのCellであればnilを返して非選択処理を弾く
    return sections[indexPath.section] == "お好きな銭湯" ? nil : indexPath
}
```
コメントにある通りです。全てコメントアウトに書きました。


## 後記
今回初めてQiitaに投稿しましたが、こういう記事・ブログ的なのを書くのは、高校生の時にアメブロ(黒歴史)ぶりなのでそわそわしています。これから少しずつ記事を書いていきたいと思います。

文化浴泉とてもいいです。三茶の八幡湯と北欧は最近行きました。北欧はズルイですね。