【Swift】セクション単位で収束できるTableViewを実装する

次の要件を満たすTableViewを実装します。
- セクションヘッダーをタップすると、セクション下にあるすべての項目を非表示にする（収束）
- 収束状態のセクションヘッダーをタップすると、セクション下にあるすべての項目を表示する（展開）

-------
### セクションデータクラスの実装
各セクションの表示状態を決定するにあたり、収束/展開どちらの状態であるかを知る必要があります。
収束の状態 `isConvergence` をプロパティに持つ `SectionData` クラスを実装します。

```
class SectionData: NSObject {
	var isConvergence: Bool = false
	var rows = [String]()
}
```

プロパティ `rows` はセクションに属するデータソースのリストです。
サンプルではString型のリストですが、用途に合わせて変更してください。

-------
### タップ可能なセクションヘッダーの実装
タップに反応するヘッダーを実装します。
タップイベントをUITableViewの実装先へ通知するまでがヘッダーの役割とします。

```
protocol TableHeaderViewDelegate: class {
	/// 収束状態の変更要求
	func changeConvergenceState(view: TableHeaderView, section: Int)
}

class TableHeaderView: UILabel {
	
	fileprivate var section: Int = 0
	weak var delegate: TableHeaderViewDelegate? = nil
	
	convenience init(section: Int) {
		self.init(frame: CGRect.zero)
		self.section = section
		self._init()
	}
}

// MARK: - fileprivate functions
fileprivate extension TableHeaderView {
	func _init() {
		self.isUserInteractionEnabled = true
		self.textAlignment = .center
		
		self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleView_Tap(sender:))))
	}
	
	@objc func handleView_Tap(sender: UITapGestureRecognizer) {
		self.delegate?.changeConvergenceState(view: self, section: self.section)
	}
}
```

サンプルでは必要最小限、セクションの情報を示す為のUILabelに機能を追加しています。
UILabelにタップイベントを追加し、 `TableHeaderViewDelegate` の　`changeConvergenceState()` メソッドをコールします。
UILabelでは `isUserInteractionEnabled` に `true` を設定しないとタップイベントが反応しないことに注意してください。


-------
### UITableViewをメンバに持つ、UIViewControllerの実装
UIViewControllerに今回の要件を実装します。
UIViewControllerにUITableViewを追加し、 `UITableViewDataSource` と　`UITableViewDelegate` を関連づけます。

```
class ViewController: UIViewController {

	@IBOutlet weak var tableView: UITableView!
	
	fileprivate lazy var sections: [SectionData] = {
		var sections = [SectionData]()
		
		for section in 0...4 {
			let data = SectionData()
			for row in 0...4 {
				data.rows.append("\(section.description): \(row.description)")
			}
			sections.append(data)
		}
		
		return sections
	}()
	
	override func viewDidLoad() {
		super.viewDidLoad()
		// Do any additional setup after loading the view, typically from a nib.
	}

	override func didReceiveMemoryWarning() {
		super.didReceiveMemoryWarning()
		// Dispose of any resources that can be recreated.
	}
}

// MARK: - UITableViewDataSource
extension ViewController: UITableViewDataSource {
	func numberOfSections(in tableView: UITableView) -> Int {
		return self.sections.count
	}
	
	func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
		let sectionData = self.sections[section]
		
		// 実装ポイント
		// セクションが収束している場合(isConvergence)、0を返します
		return sectionData.isConvergence ? 0 : sectionData.rows.count
	}
	
	func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
		var cell = self.tableView.dequeueReusableCell(withIdentifier: "cell")
		if cell == nil {
			cell = UITableViewCell(style: .default, reuseIdentifier: "cell")
		}
		
		cell?.textLabel?.text = self.sections[indexPath.section].rows[indexPath.row]
		
		return cell!
	}
}

// MARK: - UITableViewDelegate
extension ViewController: UITableViewDelegate {
	func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
		return 60.0
	}
	
	func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
		let view = TableHeaderView(section: section)
		view.delegate = self
		view.text = "section \(section.description) header"
		view.backgroundColor = #colorLiteral(red: 0.721568644, green: 0.8862745166, blue: 0.5921568871, alpha: 1)
		return view
	}
}

// MARK: - TableHeaderViewDelegate
extension ViewController: TableHeaderViewDelegate {
	func changeConvergenceState(view: TableHeaderView, section: Int) {
		// 実装ポイント
		// セクションがタップされた際、該当するセクションの収束状態を反転し、リロードします
		self.sections[section].isConvergence = !self.sections[section].isConvergence
		self.tableView.reloadSections(IndexSet([section]), with: .automatic)
	}
}
```

`sections` プロパティにはテーブルに表示するすべてのデータが入ります。

セクションの行数を返すメソッド `tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int` では、収束の状態により返す値を変えています。
収束している場合は0を、展開している場合は実データの数を返します。

セクションヘッダーを返すメソッド 'tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?' では、先ほど実装した `TableHeaderView` を返します。
その際 `delegate` を関連づけることで、 `changeConvergenceState(view: TableHeaderView, section: Int)` がコールされるようになります。

セクションヘッダーがタップされた際にコールされるメソッド `changeConvergenceState(view: TableHeaderView, section: Int)` では、該当するセクションの収束状態を反転し、リロードを行います。
リロードを行うことで `tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int` がコールされ、収束状態にあった表示に更新されます。


-------
### データ操作を行う際の注意事項
セクションの収束状態を表現するため、 `tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int` では実データに合わない数（0）を返しています。
その為、無条件にTableViewに対して `insert` や `move` などを行うと例外が発生しますので注意が必要です。
実装の際は特に収束状態のセクションに気をつけてください。

##### 追加の場合
セクションが収束している場合、tableViewに `insert` は行わない

##### 更新の場合
セクションが収束している場合、tableViewに `reload` は行わない

##### 削除の場合
セクションが収束している場合、tableViewに `delete` は行わない

##### 移動の場合
###### 移動元セクションが収束している、移動先セクションが収束していない場合
`moveRow` は使わない。移動先セクションに対して `insert` を行う
###### 移動元セクションが収束していない、移動先セクションが収束している場合
`moveRow` は使わない。移動元セクションに対して `delete` を行う
###### 移動元/移動先共に収束している
tableViewに対する操作は行わない


-------
### サンプルコード
サンプルをGitHubに公開しています。
[https://github.com/ICTFractal/ConvergenceSectionTableViewSample/tree/master](https://github.com/ICTFractal/ConvergenceSectionTableViewSample/tree/master "ConvergenceSectionTableViewSample")