[Swift]UITableViewCellをEnumで管理する

#はじめに
UITableViewCellのパターンが増えると、カオスになりがちな処理を
enumを利用して実装してみます。

※よくあるパターンなので、自分の中で一度整理してみます。

enumを利用する理由は、下記の2点です。

1. セルタイプのパターンを網羅的に管理するため
2. セルタイプのパターン毎に処理を纏めて管理するため

## 今回ご説明する例
下記の3パターンのセルを出し分ける例でご説明します。

1. データが0件の場合は、「該当するデータはありません」と表示する
2. 一覧にタイトルを左寄せで表示する
3. 一覧にタイトルを中央寄せで表示する

※本当は、全く違うタイプのセルでご説明したほうが良いと思いますが、
　お話のメインがUITableViewCellをEnumで管理することなので、
　シンプルなセルのタイプでご説明します。

## 今回ご説明しないこと
Storyboardを利用したTableViewやTableViewCellを生成する部分は、
割愛させていただきます。

## 実装手順
下記のとおりです。

1. セルタイプのパターンをenumで定義する
2. 一覧表示管理用の構造体を定義する
3. パターン毎にTableViewCellを定義する
4. enumにパターン毎の各種処理を定義する
5. セルタイプ取得用のクラスを定義する
6. UITableView生成用のクラスを定義する
7. UIViewControllerから利用してみる

# Let'S Try
## 1. セルタイプのパターンをenumで定義する
3パターンのセルを定義します。

```swift:CellType.swift
import UIKit

enum CellType {
    case textAliginLeft
    case textAliginCenter
    case noData
}
```

## 2. 一覧表示管理用の構造体を定義する

```swift:Item.swift
struct Item {
    var title = ""
    var type = CellType.noData
}
```

## 3. パターン毎にTableViewCellを定義する
### 3.1. TableViewCellのリユースIDを定義する
今回は、クラス名とリユースIDを同名とするため、下記のように定義します。

```swift:UITableViewCell+Helper.swift
import UIKit

protocol UITableViewCellType {
    static var reuseIdentifier: String { get }
}

extension UITableViewCell: UITableViewCellType {
    internal static var reuseIdentifier: String {
        return String(describing: self)
    }
}
```

### 3.2. 一覧用のTableViewCellを作る
タイトル表示用のラベルがあるだけのセルです。

```swift:TextAlignLeftTableViewCell.swift
import UIKit

final class TextAlignLeftTableViewCell: UITableViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    
    func configForCell(item: Item) {
        titleLabel.text = item.title
    }
}
```

```swift:TextAlignCenterTableViewCell.swift
import UIKit

final class TextAlignCenterTableViewCell: UITableViewCell {
    @IBOutlet weak var titleLabel: UILabel!
    
    func configForCell(item: Item) {
        titleLabel.text = item.title
    }
}
```

### 3.3. データが0件の場合のTableViewCellを作る
タイトル表示用のラベルがあるだけのセルです。

```swift:ListTableViewCell.swift
import UIKit

final class NoDataTableViewCell: UITableViewCell {    
    @IBOutlet weak var titleLabel: UILabel!
    
    func configForCell(title: String) {
        titleLabel.text = title
    }
}
```

## 4. enumにパターン毎の各種処理を定義する

```swift:CellType.swift
import UIKit

enum CellType {
    case textAliginLeft
    case textAliginCenter
    case noData
    
    // セルタイプ毎のTableViewCellのインスタンスを取得する
    func cell(tableView: UITableView, indexPath: IndexPath) -> UITableViewCell {
        
        switch self {
        case .textAliginLeft:
            return tableView.dequeueReusableCell(withIdentifier: TextAlignLeftTableViewCell.reuseIdentifier,
                                                 for: indexPath as IndexPath) as! TextAlignLeftTableViewCell

        case .textAliginCenter:
            return tableView.dequeueReusableCell(withIdentifier: TextAlignCenterTableViewCell.reuseIdentifier,
                                                 for: indexPath as IndexPath) as! TextAlignCenterTableViewCell
            
        case .noData:
            return tableView.dequeueReusableCell(withIdentifier: NoDataTableViewCell.reuseIdentifier,
                                                 for: indexPath as IndexPath) as! NoDataTableViewCell
        }
    }
    
    // セルタイプ毎に値を設定する
    func configCellForList(cell: UITableViewCell,
                               indexPath: IndexPath,
                               items: [Item]) {
        
        switch self {
        case .textAliginLeft:
            let cell = cell as! TextAlignLeftTableViewCell
            cell.configForCell(item: items[indexPath.row])

        case .textAliginCenter:
            let cell = cell as! TextAlignCenterTableViewCell
            cell.configForCell(item: items[indexPath.row])
            
        case .noData:
            let cell = cell as! NoDataTableViewCell
            cell.configForCell(title: "該当するデータはありません")
        }
    }
    
    // セルタイプ毎にセルの高さを取得する
    func heightForRow() -> CGFloat {
        
        switch self {
        case .textAliginLeft, .textAliginCenter:
            return 44.0
        default:
            return UIScreen.main.bounds.height
        }
    }
}
```

## 5. セルタイプ取得用のクラスを定義する
データ件数が0件の場合は、noDataセルを利用します。
それ以外は、指定したセルを利用します。

```swift:UITableView+CellType.swift
import UIKit

extension UITableView {

    func cellType(indexPath: IndexPath, items: [Item]) -> CellType {
        if items.count == 0 { return .noData }
        return items[indexPath.row].type
    }
}
```

## 6. UITableView生成用のクラスを定義する

```swift:ListDataSource.swift
import UIKit

final class ListDataSource:NSObject, UITableViewDataSource {
    
    fileprivate var items: [Item] = []
    
    func setItems(items: [Item]) {
        self.items = items
    }
    
    func tableView(_ tableView: UITableView,
                   numberOfRowsInSection section: Int) -> Int {        
        return items.count == 0 ? 1 : items.count
    }
    
    func tableView(_ tableView: UITableView,
                   cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cellType = tableView.cellType(indexPath: indexPath,
                                              items: items)
        let cell = cellType.cell(tableView: tableView,
                                 indexPath: indexPath)
        cellType.configCellForList(cell: cell,
                              indexPath: indexPath,
                                  items: items)
        return cell
    }
}
```

## 7. UIViewControllerから利用してみる

```swift:UIViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    let dataSource = ListDataSource()
    var items: [Item] = [
        Item(title: "title1", type: CellType.textAliginLeft),
        Item(title: "title2", type: CellType.textAliginCenter)
    ]
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        loadList()
    }
    
    fileprivate func setup() {
        tableView.dataSource = dataSource
        tableView.delegate = self
    }
    
    fileprivate func loadList() {
        dataSource.setItems(items: items)
    }
}

//MARK: - UITableViewDelegate

extension ViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView,
                   heightForRowAt indexPath: IndexPath) -> CGFloat {
        
        let cellType = tableView.cellType(indexPath: indexPath,
                                              items: items)
        return cellType.heightForRow()
    }
}
```

# まとめ
新しいUITableViewCellのパターンが増えると、
該当のTableViewCellとenumを拡張していくイメージです。

拡張していってもUIViewControllerは肥大化しないし、
enumで定義しているので追加漏れもありません。

enumが肥大化していく問題は別途考えたいと思います。