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

More than 1 year has passed since last update.

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

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

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

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

目指す形はこうです。

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

Registrableプロトコル

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

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を使うようにします。

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に限定しています。

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のエクステンションに追加します。戻り値のジェネリクスを使ってクラスを指定できるようにしています。

    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に適合させます。プロトコルエクステンションがあるので、メソッドを実装する必要はありません。

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

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

registerは以下の通り。

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

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

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

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

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

さらに…

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

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
    }
}

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

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

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

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