【Swift】世界一わかりやすいTableViewのアコーディオンの実装方法

# はじめに
梅雨前に新しい靴買ううやつ大体梅雨舐めてる、どうもこんにちはTOSHです！
アコーディオンの実装してみようって思ったけど、世の中に書いてある記事分かり難すぎん...?
ということで、きちんと、書いてみようかなと思い立ってやってみました。
実際そんなに難しくないのに、記事がわかりにくいせいで、難しく思えちゃってたので、世界一わかりやすく説明して行こうと思います！

# アコーディオンとは
これです！
![](https://media.giphy.com/media/J5XpoRmmLNmsLmsoAp/giphy.gif)
よく不動産やバイト系のアプリに使用されているイメージですねー

# サンプルコード
[GitHub](https://github.com/tosh7/Accordion_Sample)

これ書けば動きます。

```ViewController.swift
import UIKit

struct rail {
    var isShown: Bool
    var railName: String
    var stationArray: [String]
}

final class ViewController: UIViewController {
    
    @IBOutlet private weak var tableView: UITableView! {
        didSet {
            tableView.dataSource = self
            tableView.delegate = self
        }
    }

    private let headerArray: [String] = ["山手線", "東横線", "田園都市線", "常磐線"]
    private let yamanoteArray: [String] = ["渋谷", "新宿", "池袋"]
    private let toyokoArray: [String] = ["自由ヶ丘", "日吉"]
    private let dentoArray: [String] = ["溝の口", "二子玉川"]
    private let jobanArray: [String] = ["上野"]

    private lazy var courseArray = [
        rail(isShown: true, railName: self.headerArray[0], stationArray: self.yamanoteArray),
        rail(isShown: false, railName: self.headerArray[1], stationArray: self.toyokoArray),
        rail(isShown: false, railName: self.headerArray[2], stationArray: self.dentoArray),
        rail(isShown: false, railName: self.headerArray[3], stationArray: self.jobanArray)
    ]
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        if courseArray[section].isShown {
            return courseArray[section].stationArray.count
        } else {
            return 0
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
        cell?.textLabel?.text = courseArray[indexPath.section].stationArray[indexPath.row]
        
        return cell!
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return courseArray.count
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return courseArray[section].railName
    }
}

extension ViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = UITableViewHeaderFooterView()
        let gesture = UITapGestureRecognizer(target: self,
                                             action: #selector(headertapped(sender:)))
        headerView.addGestureRecognizer(gesture)
        headerView.tag = section
        return headerView
    }

    @objc func headertapped(sender: UITapGestureRecognizer) {
        guard let section = sender.view?.tag else {
            return
        }
        courseArray[section].isShown.toggle()
        
        tableView.beginUpdates()
        tableView.reloadSections([section], with: .automatic)
        tableView.endUpdates()
    }
}
```


# 実装の簡単流れ
- Modelを作成する
- sectionごとにリストを表示するようにする
- Headerのタップを感知する
- タップによって表示非表示を反転させる

# 実装説明
では、実際に説明に入ります！
### Modelを作成する
まずは表示を行うためのモデルを作成しましょう！今回のケースでは、そのもでは持つべき情報は、
- 表示をされるか否か
- 路線名
- 駅名
の三つになります。
それに対応する、モデルを作成するとこちらのようになります。

```swift
struct rail {
    var isShown: Bool
    var railName: String
    var stationArray: [String]
}
```

今回生成するものは、静的なモデルで良いので、こんかいはこのように、先ほどのモデルを配列にします。配列にしておくことで、後々TableViewにセットする際に、使いやすい形になると思います。

```swift
private let headerArray: [String] = ["山手線", "東横線", "田園都市線", "常磐線"]
private let yamanoteArray: [String] = ["渋谷", "新宿", "池袋"]
private let toyokoArray: [String] = ["自由ヶ丘", "日吉"]
private let dentoArray: [String] = ["溝の口", "二子玉川"]
private let jobanArray: [String] = ["上野"]

//今回使用するモデルの配列
private lazy var courseArray = [
    rail(isShown: true, railName: self.headerArray[0], stationArray: self.yamanoteArray),
    rail(isShown: false, railName: self.headerArray[1], stationArray: self.toyokoArray),
    rail(isShown: false, railName: self.headerArray[2], stationArray: self.dentoArray),
    rail(isShown: false, railName: self.headerArray[3], stationArray: self.jobanArray)
]
```
モデルの配列のみ、lazyをつけてあげるようにしましょう！詳しい説明はここでは、割愛させていただきます。
これらの変数は基本的にダイブからアクセスされることがないので、privateにしていますが、ここはあくまで好みで。

### sectionごとにリストを表示するようにする
ここでの設定は基本的に、通常のTableViewの作成方法となんら変わりはありません。

```swift
final class ViewController: UIViewController {
    
    //tableViewのdelegateとdataSourceをセットしましょう！
    @IBOutlet private weak var tableView: UITableView! {
        didSet {
            tableView.dataSource = self
            tableView.delegate = self
        }
    }
}
```
次に、DataSourceを設定して行きます。ここでは、主に。TableViewの中身を管理していると思うとわかりやすいのではないでしょうか？

```swift
extension ViewController: UITableViewDataSource {
    //各セッションの中のcellの数をここでセットしています。
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return courseArray[section].stationArray.count
    }

    //ここでは、cellのなかに何が入るのかを設定しています。cellはtitleLabelを元々持っているのでそのまま使ってしまいましょう！
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell")
        cell?.textLabel?.text = courseArray[indexPath.section].stationArray[indexPath.row]
        
        return cell!
    }

    //ここではセクションの数を設定しています。今回は、路線の数だけ、セクションが必要になります。
    func numberOfSections(in tableView: UITableView) -> Int {
        return courseArray.count
    }

    //ここでは、セクションのタイトルに何を入れるかをセットしています。
    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return courseArray[section].railName
    }
}
```

ここまで、書いてコンパイルをすると、このような状態になると思います！
![スクリーンショット 2020-06-22 17.19.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/180994/8dbf016d-641f-5048-a1aa-82edee8893f4.png)
まだこの状態では、肝心のアコーディオンはできないので、次の項目からは、その設定をしていこうと思います！

### Headerのタップを感知する
Headerのタップ有無でアコーディオン状態を切り替えるのですが、現在のままではタップを感知できていません。
ので、HeaderにUITapGestureを設定する必要があります。こちらは、UITableViewDelegateで設定をして行きます。

```swift
extension ViewController: UITableViewDelegate {
    //HeaderのViewに対して、タップを感知できるようにして行きます。
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = UITableViewHeaderFooterView()
        //UITapGestureを定義する。Tapされた際に、headertappedを呼ぶようにしています。
        let gesture = UITapGestureRecognizer(target: self,
                                             action: #selector(headertapped(sender:)))
        //ここで、実際に、HeaderViewをセットします。
        headerView.addGestureRecognizer(gesture)

        headerView.tag = section
        return headerView
    }

    //タップされるとこのメソッドが呼ばれます。
    @objc func headertapped(sender: UITapGestureRecognizer) {
        print("タップされたよ")
    }
}
```


### タップによって表示非表示を反転させる
先ほどのフェーズで、タップは感知出来たので、次は表示、非表示を切り替えましょう

```swift
extension ViewController: UITableViewDelegate {
    ~~略~~

    //タップされるとこのメソッドが呼ばれます。
    @objc func headertapped(sender: UITapGestureRecognizer) {
       //tagを持っていない場合は、guardします。
        guard let section = sender.view?.tag else {
            return
        }
        //courseArray[section].isShownの値を反転させます。
        courseArray[section].isShown.toggle()
       
        //これ以降で表示、非表示を切り替えます。
        tableView.beginUpdates()
        tableView.reloadSections([section], with: .automatic)
        tableView.endUpdates()
    }
}
```
これで、isShownを判定させて、再描画を呼び鵜出すことが出来たので、TableViewのDataSourceでも設定を行って行きましょう！

```swift
extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        //courseArray[section].isShownの値によって、表示数を変更
        if courseArray[section].isShown {
            return courseArray[section].stationArray.count
        } else {
            return 0
        }
    }
 ~~略~~
}
```

ちなみに、個人的には、こんな書き方の方が好きだったりします。

```swift
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {        
        return courseArray[section].isShown ? courseArray[section].stationArray.count : 0
    }
```

これで、表示、非表示を切り替えることができました！
このようになるはずです。
![](https://media.giphy.com/media/J5XpoRmmLNmsLmsoAp/giphy.gif)

# まとめ
ポイントは、headerのタップをきちんと感知することかなと思います！
よかったら、スターお願いします！
https://github.com/tosh7/Accordion_Sample

# 参考にしたサイト
[基本的な実装方法、これがいちばん参考になったかも](https://qiita.com/woxtu/items/c2e3fcfb5fd2005b43e2)
[アコーディオンのアニメーションの種類](https://qiita.com/akatsuki174/items/20ed135b28f44be89b9b)