意外と大変！？アコーディオン(折り畳み)メニューの作り方

どこかの記事で「iOSアプリでアコーディオンメニューを作るのは意外と大変:sweat:」というのを見て、作ったことなかったのでやってみました:smile:
お題としては、何か商品(今回は服)を検索する時のフィルター画面をイメージしました:+1:
(ちなみに最近流行りの`RxDataSources`を使ってcollectionViewでやろうとしたら数時間かかった末に挫折。誰かお手本お願いします。:sweat:)

こんな感じです。
<img src="https://qiita-image-store.s3.amazonaws.com/0/106804/62d960b7-b389-5f1d-3b0f-3eccaa458a87.gif" width="200">

[ここに置いてます(github):+1:](https://github.com/HiromiSakurai/accordion-menu-sample)

## データ構造
おそらくここ大事。この構造を間違えるとややこしくなりそう。。。

```swift
struct Section {
    var title: String
    var values: [(title: String, checked: Bool)]
    var expanded: Bool
}
```
セクション単位でデータを管理する。`values`の配列がセクションの中にあるセルのデータ(タイトルとチェックの可否)です。

## UITabaleViewの作成
※本筋に関係ないとこは省略

```swift
class MenuViewController: UIViewController {

    @IBOutlet weak var menuTableView: UITableView!

    private var sections: [Section] = [Section(title: "Gender",
                                               values: [("MEN", false),
                                                        ("WOMEN", false),
                                                        ("KIDS", false)],
                                               expanded: true),
                                       Section(title: "Size",
                                               values: [("X", false),
                                                        ("M", false),
                                                        ("L", false)],
                                               expanded: true),
                                       Section(title: "Clothes",
                                               values: [("T-Shirt", false),
                                                        ("Bottom", false),
                                                        ("Shoes", false)],
                                               expanded: true)]

    override func viewDidLoad() {
        super.viewDidLoad()

        setupTableView()
    }

    private func setupTableView() {
        menuTableView.dataSource = self
        menuTableView.delegate = self
        menuTableView.register(MenuTableViewCell.self, forCellReuseIdentifier: "cell")
    }
}

extension MenuViewController: UITableViewDataSource {

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

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return sections[section].values.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! MenuTableViewCell

        // cellにデータをセット
        cell.titleLabel.text = sections[indexPath.section].values[indexPath.row].title
        if self.sections[indexPath.section].values[indexPath.row].checked {
            cell.checkBoxButton.setImage(checkedBoxImage, for: .normal)
        } else {
            cell.checkBoxButton.setImage(unCheckedBoxImage, for: .normal)
        }

　　　　　　　　　　　　　　　　// cellがタップされた時の操作をクロージャにして渡す
        cell.tappedHandler = { [unowned self] in
            if self.sections[indexPath.section].values[indexPath.row].checked {
                cell.checkBoxButton.setImage(unCheckedBoxImage, for: .normal)
            } else {
                cell.checkBoxButton.setImage(checkedBoxImage, for: .normal)
            }
            self.sections[indexPath.section].values[indexPath.row].checked = !self.sections[indexPath.section].values[indexPath.row].checked
        }
        return cell
    }
}

extension MenuViewController: UITableViewDelegate {

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if sections[indexPath.section].expanded {
            return 60
        } else {
            return 0 //　高さを0にすることで折りたたむ
        }
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = SectionHeaderView()

        // ヘッダーのタイトル&セクション番号&ヘッダー自身がタップされた時の操作を設定
        headerView.config(title: sections[section].title, section: section) { [unowned self] section in
            self.sections[section].expanded = !self.sections[section].expanded
            self.menuTableView.beginUpdates() //tableの更新をスタート
            for i in 0 ..< self.sections[section].values.count {
                // タップされたセクションヘッダー配下のcellだけ更新する
                // 3行上で`expanded: Bool`を逆転させているので、cellの折り畳みorその逆が実現できる
                self.menuTableView.reloadRows(at: [IndexPath(row: i, section: section)], with: .automatic)
            }
            self.menuTableView.endUpdates() // 更新終了
        }
        return headerView
    }
}

```

ポイントとしては、

- セルとヘッダーがタップされた時の処理をクロージャとしてViewControllerから渡してやる
- セクションの折り畳みは、配下のセルの高さを0にすることで表現する
- その際に、テーブル全体を`reloadData()`せずに(パフォーマンス的にBAD&テーブルがチラつく)、特定のセルだけ更新する

って感じだと思います。

## ヘッダークラスの作成

```swift
class SectionHeaderView: UITableViewHeaderFooterView {

    var tappedHandler: ((Int) -> Void)?
    var section: Int?

    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        
        // 自身がタップされたことを検知する
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleSelected)))
    }

    func config(title: String, section: Int, tappedHandler: @escaping (Int) -> Void) {
        self.textLabel?.text = title
        self.section = section
        self.tappedHandler = tappedHandler
    }

    // 自身がタップされた時に、ViewControllerから渡された処理を実行する
    @objc private func handleSelected(gestureRecognizer: UITapGestureRecognizer) {
        guard let headerView = gestureRecognizer.view as? SectionHeaderView,
            let section = headerView.section,
            let tappedHandler = tappedHandler else { return }
        tappedHandler(section)
    }
}
```

## テーブルセルの設定
ここはほとんど特筆すべき点はなさげ。。。

```swift
class MenuTableViewCell: UITableViewCell {

    var tappedHandler: (() -> Void)?

    lazy var checkBoxButton: UIButton = {
        let btn = UIButton()
        btn.setImage(unCheckedBoxImage, for: .normal)
        btn.addTarget(self, action: #selector(toggleCheckBox), for: .touchUpInside)
        btn.translatesAutoresizingMaskIntoConstraints = false
        return btn
    }()

    // 外から(`UITableViewDelegate`の関数)設定された処理を実行
    @objc private func toggleCheckBox() {
        guard let handler = tappedHandler else { return }
        handler()
    }
}
```

## 終わりに
今回は単純なものだったから、そうでもなかったけど、構造が複雑になってきたら確かに大変そうや。。。
全然もっといいやり方がありそうなので、皆さんのアイデアやご意見、アドバイスお待ちしています:smile:

Thanks:exclamation::exclamation: