[Swift] アコーディオンメニューを作ってみた（ExpandableTableView）

# はじめに
アコーディオンメニューを作る機会がありましたので、
メモを共有します。

<img width="332" alt="スクリーンショット 2016-09-01 12.26.01.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/373c867d-862c-b2fc-2a46-9dbae1a35dc9.png">

## 設計方針
1. TableViewのセクションをアコーディオンの親Viewとする
2. TableViewのローをアコーディオンの子Viewとする
3. アコーディオンは、1つのセクションのみ開くものとする

# 早速実装してみます！

## (1) プロトコルを検討する
・アコーディオンを開く
・アコーディオンを閉じる

```swift:HeaderView.swift
import UIKit

protocol HeaderViewDelegate {
    func openedView(section: Int)
    func closedView(section: Int)
}
```

## (2) セクションをタップされたときのアクションを検討する
・セクションにタップイベントを付加する
・該当セクションが開いていれば、閉じる
・該当セクションが閉じていれば、開く

```swift:HeaderView.swift
class HeaderView: UIView {

    var tableView:ExpandableTableView!
    var delegate:HeaderViewDelegate?
    var section = 0

    required init(tableView:ExpandableTableView, section:Int){

        guard let height = tableView.delegate?.tableView!(tableView, heightForHeaderInSection: section) else{
            fatalError("heightForHeaderInSectionを呼んでね。")
        }

        let frame = CGRectMake(0, 0, CGRectGetWidth(tableView.frame), height)
        super.init(frame: frame)
        
        self.tableView = tableView
        self.delegate = tableView
        self.section = section
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) は使わないでね。")
    }
    
    override func layoutSubviews() {

        let toggleButton = UIButton()
        toggleButton.addTarget(self, action: #selector(self.toggle(_:)), forControlEvents: UIControlEvents.TouchUpInside)
        toggleButton.backgroundColor = UIColor.clearColor()
        toggleButton.frame = CGRectMake(0, 0, self.frame.width, self.frame.height)
        self.addSubview(toggleButton)
    }
    
    func toggle(sender:AnyObject){
        
        if tableView.sectionOpen != section {
            delegate?.openedView(section)
        } else if tableView.sectionOpen != NSNotFound {
            delegate?.closedView(self.tableView!.sectionOpen)
        }
    }
}
```

## (3) 現在開いているセクションの番号を保持する

```swift:ExpandableTableView.swift
import UIKit

class ExpandableTableView : UITableView {
    var sectionOpen = NSNotFound
}
```

## (4) TableViewにプロトコルを実装する

```swift:ExpandableTableView.swift
extension ExpandableTableView: HeaderViewDelegate {

    func openedView(section: Int) {

        if self.sectionOpen != NSNotFound {
            closedView(self.sectionOpen)
        }

        self.sectionOpen = section

        if let numberOfRows = self.dataSource?.tableView(self, numberOfRowsInSection: section) {

            var indexesPathToInsert:[NSIndexPath] = []

            for i in 0 ..< numberOfRows {
                indexesPathToInsert.append(NSIndexPath(forRow: i, inSection: section))
            }

            if indexesPathToInsert.count > 0 {
                self.beginUpdates()
                self.insertRowsAtIndexPaths(indexesPathToInsert, withRowAnimation: UITableViewRowAnimation.Automatic)
                self.endUpdates()
            }
        }
    }

    func closedView(section: Int) {

        if let numberOfRows = self.dataSource?.tableView(self, numberOfRowsInSection: section) {
            var indexesPathToDelete:[NSIndexPath] = []
            self.sectionOpen = NSNotFound

            for i in 0 ..< numberOfRows {
                indexesPathToDelete.append(NSIndexPath(forRow: i, inSection: section))
            }

            if indexesPathToDelete.count > 0 {
                self.beginUpdates()
                self.deleteRowsAtIndexPaths(indexesPathToDelete, withRowAnimation: UITableViewRowAnimation.Top)
                self.endUpdates()
            }
        }
    }
}
```

## (5) Custom ClassにExpandableTableViewを指定する

<img width="684" alt="スクリーンショット 2016-09-01 12.05.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/71694/978cbae1-54d1-e7ba-b33c-eb78039070f2.png">

## (6) 表示するデータを二次元配列で用意する

```swift:ViewController.swift
import UIKit

class ViewController: UIViewController  {
    
    @IBOutlet weak var tableView: ExpandableTableView!
    var items:[[Int]] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ダミーデータ
        for i in 0 ..< 20 {
            items.append([])
            for j in 0 ..< 10 {
                items[i].append(j)
            }
        }
    }
}

extension ViewController: UITableViewDataSource {

    func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return CGFloat(44.0)
    }

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return items.count
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        if self.tableView.sectionOpen != NSNotFound && section == self.tableView.sectionOpen {
            return items[section].count
        }
        return 0
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath)
        cell.textLabel?.text = "section \(indexPath.section) row \(indexPath.row)"
        cell.textLabel?.backgroundColor = UIColor.clearColor()
        return cell
    }
}

extension ViewController: UITableViewDelegate {

    func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {

        let headerView = HeaderView(tableView: self.tableView, section: section)
        headerView.backgroundColor = UIColor.lightGrayColor()
        let label = UILabel(frame: headerView.frame)
        label.text = "Section \(section)"
        label.textColor = UIColor.whiteColor()
        headerView.addSubview(label)
        return headerView
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        tableView.deselectRowAtIndexPath(indexPath, animated: true)
    }
}
```

# 最後に
サンプルは、[Github](https://github.com/stv-ekushida/ios-expandable-list)を御覧ください。
(Swift3対応しました。）

以上です。