伸縮するUITableViewを作る

# 確認時の環境
Xcode 9.2 / iOS 11.2 / Swift 4

# 実装イメージ
Headerをタップでセクション全部を伸縮、セル内のボタン押下でそのセルだけ伸縮するようなリストを作ってみます。

![hoge.gif](https://qiita-image-store.s3.amazonaws.com/0/116677/43c75d5c-41f0-01d3-dbe1-97ba03a08f1b.gif)


# Headerタップでセクションを伸縮させてみる

## UITableViewHeaderFooterViewを継承したクラスを作成
タップしたら、自作のdelegateをfireするだけのシンプルな作りです。

```swift
protocol ExpandableHeaderViewDelegate {
    func toggleSection(header: ExpandableHeaderView, section: Int)
}

class ExpandableHeaderView: UITableViewHeaderFooterView {

    @IBOutlet weak var titleLabel: UILabel!
    @IBOutlet weak var iconImageView: UIImageView!

    var delegate: ExpandableHeaderViewDelegate?
    var section: Int!

    override func awakeFromNib() {
        self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(selectHeaderAction)))
    }

    @objc func selectHeaderAction(gestureRecognizer: UITapGestureRecognizer) {
        let cell = gestureRecognizer.view as! ExpandableHeaderView
        delegate?.toggleSection(header: self, section: cell.section)
    }

    override func layoutSubviews() {
        super.layoutSubviews()
    }

}
```

## Xibファイルを作成
xibで以下のようなViewを作りました。
前に作ったExpandableHeaderViewを忘れずに登録しておきましょう。

![header.png](https://qiita-image-store.s3.amazonaws.com/0/116677/aabf9cb7-8cda-f40c-5a38-b4ab95b76b01.png)

## 実装してみる
大事なところだけ書きます。

```swift
class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()

        let nib = UINib(nibName: "ExpandableHeaderView", bundle: nil)
        tableView.register(nib, forHeaderFooterViewReuseIdentifier: "ExpandableHeaderView")

        tableView.dataSource = self

    }
}
```
```swift

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 44
    }

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 2
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = self.tableView.dequeueReusableHeaderFooterView(withIdentifier: "ExpandableHeaderView") as! ExpandableHeaderView
        header.titleLabel.text = sections[section].title
        header.section = section
        header.delegate = self
        header.iconImageView.backgroundColor = sections[section].expanded ? .red : .yellow
        return header
    }
}
```
```swift
extension ViewController: ExpandableHeaderViewDelegate {
    func toggleSection(header: ExpandableHeaderView, section: Int) {
        sections[section].expanded = !sections[section].expanded

        // ヘッダー自体の変更が可能
        header.iconImageView.backgroundColor = sections[section].expanded ? .red : .yellow

        tableView.beginUpdates()

        // セクション内のセルを全部リロードして、expandを表現
        for i in 0 ..< sections[section].elements.count {
            tableView.reloadRows(at: [IndexPath(row: i, section: section)], with: .automatic)
        }

        tableView.endUpdates()
    }
}
```

# セル自体を伸縮させてみる
## UITableViewCellを継承したクラスを作成
カスタムセルの作り方・使い方は、既にたくさん情報が転がってますので割愛。

```swift
class ExpandableCell: UITableViewCell {

    @IBOutlet weak var mainView: UIView!
    @IBOutlet weak var subView: UIView!

    @IBOutlet weak var mainLabel: UILabel!
    @IBOutlet weak var subLabel: UILabel!

    @IBOutlet weak var expandButton: UIButton!

    override func awakeFromNib() {
        super.awakeFromNib()
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)
    }

    func expand() {
        if self.subView.isHidden {
            self.subView.isHidden = false
            self.subLabel.isHidden = false
        }
    }

    func contract() {
        if !self.subView.isHidden {
            UIView.animate(withDuration: 0.3) {
                self.subLabel.isHidden = true
                self.subView.isHidden = true
            }
        }
    }
}
```
縮小する方だけアニメーションしてくれなかったので、自分で付けて解決。


## Storyboard上でCellをレイアウト
StackViewを使った実装です。
Buttonに接続するActionは、Cellの方ではなく、ViewControllerの方で接続します。

![スクリーンショット 2017-12-22 01.53.03.png](https://qiita-image-store.s3.amazonaws.com/0/116677/b9e18e32-1477-28f3-8660-db2cea48c5c6.png)

青の方が常に表示されるView、赤の方が出たり隠れたりするViewです。

### 細かい設定
（赤・青両方とも高さを57ptに設定するケースです）

#### Cellの高さ
TableViewの設定で、Cellの高さを最低57pt、Cellの高さは自動にしておきます。

![スクリーンショット 2017-12-22 01.56.37.png](https://qiita-image-store.s3.amazonaws.com/0/116677/d119b932-25bc-1042-f0df-9f4bc0dcce4f.png)

#### 各Viewの高さ
Cellの高さは倍の114ptに設定します。

![スクリーンショット 2017-12-22 01.57.04.png](https://qiita-image-store.s3.amazonaws.com/0/116677/9174fc36-e89c-486d-4255-27918dda4545.png)

#### 各Viewの制約
Viewの制約で、高さを57pt固定にします。
その際、heightのPriorityと、Content Compression Resistance Priority(今回はVerticalのみ）の２箇所を、以下の通り設定してください。

常に表示される側はpriorityをrequired(1000)、伸縮する方はpriorityをhigh(750)に設定します。
これによって、Cellの高さが変わったりStackView内でhiddenになる時、変更されるViewを伸縮する方に仕向けることができます。

![スクリーンショット 2017-12-22 01.58.27.png](https://qiita-image-store.s3.amazonaws.com/0/116677/5d4c3cef-7fae-92e3-bb6f-37bca2064bac.png)

![スクリーンショット 2017-12-22 01.57.55.png](https://qiita-image-store.s3.amazonaws.com/0/116677/feefa46c-289d-9e82-71d7-65ae963b6397.png)


## 使ってみる
大事なところだけ書きます。

```swift
class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.dataSource = self
    }

    @IBAction func expandButtonDidTap(_ sender: UIButton) {
        // button -> view -> stackView -> TableCell
        let cell = sender.superview?.superview?.superview?.superview as! ExpandableCell

        guard let indexPath = tableView.indexPath(for: cell) else { return }

        sections[indexPath.section].elements[indexPath.row].expanded = !sections[indexPath.section].elements[indexPath.row].expanded

        tableView.beginUpdates()

        if sections[indexPath.section].elements[indexPath.row].expanded {
            cell.expand()
        } else {
            cell.contract()
        }

        tableView.endUpdates()
    }

}
```

```swift
extension ViewController: UITableViewDataSource {

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

        cell.mainLabel.text = sections[indexPath.section].elements[indexPath.row].title
        cell.subLabel.text = "\(sections[indexPath.section].elements[indexPath.row].title!)-Sub"

        cell.subView.isHidden = !self.sections[indexPath.section].elements[indexPath.row].expanded

        return cell
    }

}
```

# おわりに

StackView、ほんと便利。

参考になれば幸いです。
~~（あとで整理してGitHubに上げたいと思います）~~

（追記）GitHubに上げました。
https://github.com/Todate/ExpandTableViewSample