Swiftでセルのregisterとdequeueをシンプルにする

table view（またはcollection view）を使うとき、多くの場合カスタムセルクラスを作ってtable viewにregisterしてからdequeueする、という決まった手順を踏むことになると思いますが、

- `reuseIdentifier`を文字列で指定しなければならない
- nibかクラスかでインターフェースが異なる
- dequeueした時にクラスが決まっていないためキャストが必要

といった難点があります。

`retuseIdentifier`についてはプライベートな文字列定数を定義したり、セルのクラス名を利用したりといった工夫をすることが考えられますが、多くのケースでセルのクラスと `reuseIdentifier` は1対1で良いので、もっとシンプルにできないか考えて見ました。

目指す形はこうです。

```swift
// MyCellクラスをregisterする
tableView.register(MyCell.self)
```
```swift
// MyCellクラスのセルをdequeueする
let cell: MyCell = tableView.dequeueReusableCell(for: indexPath)
```

# Registrableプロトコル

まず、カスタムセルが適合すべき`Registrable`プロトコルを作って、`reuseIdentifier`の文字列を返すプロパティを定義します。

```swift
protocol Registrable: class {
    static var reuseIdentifier: String { get }
}
```

続いてプロトコルエクステンションを使って、`reuseIdentifier`はデフォルトでクラス名を返すように実装します。

```
extension Registrable {
    static var reuseIdentifier: String {
        return String(describing: self)
    }
}
```

さらに、Nibで作ったセルのために、`Registrable`を継承した`NibRegistrable`プロトコルを作って、デフォルトでクラスと同名のxibファイルからNibを返せるように実装します。対象性を考慮してクラスを登録するものについては`ClassRegistrable`を使うようにします。

```swift
protocol ClassRegistrable: Registrable { }

protocol NibRegistrable: Registrable {
    static var nib: UINib { get }
}

extension NibRegistrable {
    static var nib: UINib {
        let nibName = String(describing: self)
        return UINib(nibName: nibName, bundle: Bundle(for: self))
    }
}
```

# TableViewのエクステンション

次に、`UITableView`のエクステンションを使って、`Registrable`なセルをregisterするメソッドを実装します。ジェネリクスのwhereを使って引数として受け取るものを`UITableViewCell`に限定しています。

```swift
extension UITableView {
    func register<T: Registrable>(_ registrableType: T.Type) where T: UITableViewCell {
        switch registrableType {
        case let nibRegistrableType as NibRegistrable.Type:
            register(nibRegistrableType.nib, forCellReuseIdentifier: nibRegistrableType.reuseIdentifier)
        case let classRegistrableType as ClassRegistrable.Type:
            register(classRegistrableType, forCellReuseIdentifier: classRegistrableType.reuseIdentifier)
        default:
            assertionFailure("\(registrableType) is unknown type")
        }
    }
}
```

最後に、指定したクラスのセルをdequeueするメソッドを`UITableView`のエクステンションに追加します。戻り値のジェネリクスを使ってクラスを指定できるようにしています。

```swift
    func dequeueReusableCell<T: Registrable>(for indexPath: IndexPath) -> T where T: UITableViewCell {
        guard let cell = dequeueReusableCell(withIdentifier: T.reuseIdentifier, for: indexPath) as? T else {
            fatalError("Could not dequeue cell with type \(T.self)")
        }
        return cell
    }
```

以上で準備が整いました。

# 使い方

使い方は、まず以下のようにxibを使わないセルは`ClassRegistrable`、xibを使うセルは`NibRegistrable`に適合させます。プロトコルエクステンションがあるので、メソッドを実装する必要はありません。

```swift
// xibを使わないセル
final class ACell: UITableViewCell, ClassRegistrable {
    ...
}

// xibを使うセル（BCell.xib）
final class BCell: UITableViewCell, NibRegistrable {
    ...
}

```

registerは以下の通り。

```swift
tableView.register(ACell.self)
tableView.register(BCell.self)
```

dequeueは以下のようになります。

```swift
let cell: MyCell = tableView.dequeueReusableCell(for: indexPath)
```

結果として冒頭に示した難点が解消されました。

- `reuseIdentifier`の文字列を見なくて済む
- クラスでもNibでも同じインターフェースでregisterできる
- dequeueした時点でセルのクラスが決まっている

# さらに…

同じ仕組みで`UITableViewHeaderFooterView`にも対応させることができます。

```swift
extension UITableView {

    //...

    func register<T: Registrable>(_ registrableType: T.Type) where T: UITableViewHeaderFooterView {
        switch registrableType {
        case let nibRegistrableType as NibRegistrable.Type:
            register(nibRegistrableType.nib, forHeaderFooterViewReuseIdentifier: nibRegistrableType.reuseIdentifier)
        case let classRegistrableType as ClassRegistrable.Type:
            register(classRegistrableType, forHeaderFooterViewReuseIdentifier: classRegistrableType.reuseIdentifier)
        default:
            assertionFailure("\(registrableType) is unknown")
        }
    }

    func dequeueReusableHeaderFooterView<T: Registrable>() -> T where T: UITableViewHeaderFooterView {
        guard let view = dequeueReusableHeaderFooterView(withIdentifier: T.reuseIdentifier) as? T else {
            fatalError("Could not dequeue HeaderFooterView with type \(T.self)")
        }
        return view
    }
}
```

以下のように、セルと同様のインターフェースで扱うことができます。

```swift
// ヘッダービューのクラスを登録
tableView.register(HeaderView.self)

// ヘッダービューをdeque
let headerView: HeaderView = tableView.dequeueReusableHeaderFooterView()
```

collection viewについても同様の仕組みが使えるはずです。