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についても同様の仕組みが使えるはずです。