Edited at

UITableViewのDataSourceをprotocolで定義しておく

More than 3 years have passed since last update.

追記: 更新版を書きました。->「UITableViewのDataSourceをprotocolで定義しておく 2 - Qiita

TableViewの中身を書くのが面倒だったので、protocolでうまい具合に制御して決まり切った書き方ができないかなぁと思いながら書いていたらそれっぽいのになったのでメモとして残しておきます。この方法がすごく良いかはまだわかりませんが、書くタイミングを失うことを恐れて今書きます。


protocolで制約を設けておく

用意したprotocolはこの3つです。必要最低限のことを書いていますが、このやり方がうまくいくのならばもっと拡張しても良いと思います。

protocol TableViewRowType {

}

protocol TableViewSectionType {
typealias Row: TableViewRowType
var rows: [Row] { get }
}

protocol TableViewDataSourceType {
typealias Section: TableViewSectionType
var sections: [Section] { get }
}


protocolに準拠した型を定義する

今回は設定画面のセルを想定します。ボタンが一つ付いたセルだったり、DisclosureIndicatorだったり、ログアウトのようなシンプルなセルだったり幾つかの種類があるとします。もちろんそれぞれタップなどをした時のアクションは異なります。

enum SettingsCellType {

case Colors
case License
case InAppPurchase
case Restore
case Version
}

struct SettingsRow: TableViewRowType {
let title: String?
let subTitle: String?
let cellType: SettingsCellType

init(title: String?, subTitle: String?, cellType: SettingsCellType) {
self.title = title
self.subTitle = subTitle
self.cellType = cellType
}
}

struct SettingsSection<Row: TableViewRowType>: TableViewSectionType {
let rows: [Row]
let title: String?

init(rows: [Row], title: String?) {
self.rows = rows
self.title = title
}
}

struct SettingsDataSource<Section: TableViewSectionType>: TableViewDataSourceType {
var sections: [Section]

init(sections: [Section]) {
self.sections = sections
}
}

こんな形にしてみました。下から見ていくと、DataSourceは TableViewDataSourceType に準拠していています。 sections 配列が格納するのは TableViewSectionType に準拠した型です。ジェネリクスにせずTableViewSectionTypeのままでもいいのですが、利用するときにダウンキャストをするのが面倒なのでジェネリクスにしています。

Sectionも同様に TableViewRowType に準拠した型格納する rows を持っています。

SettigsRowはとりあえず、 titlesubtitlecellTypeを持たせました。 cellType は後ほどアクションやセルの見た目を分岐するために利用します。表示するセルの数だけあると考えれば良いと思います。


UIViewControllerから利用する

dataSource に値を格納するところの書き方は人それぞれ好みは分かれそうですがこんな感じになりました。まずは、 viewDidLoad の周辺です。

viewDidLoad

class SettingsViewController: UITableViewController {

var dataSource: SettingsDataSource<SettingsSection<SettingsRow>>!

override func viewDidLoad() {
super.viewDidLoad()

dataSource = SettingsDataSource<SettingsSection<SettingsRow>>(sections: [
SettingsSection<SettingsRow>(rows: [
SettingsRow(title: "Colors", subTitle: nil, cellType: .Colors)
], title: "General"),
SettingsSection<SettingsRow>(rows: [
SettingsRow(title: "InAppPurchase", subTitle: nil, cellType: .InAppPurchase),
SettingsRow(title: "Restore", subTitle: nil, cellType: .Restore)
], title: "Purchase"),
SettingsSection<SettingsRow>(rows: [
SettingsRow(title: "License", subTitle: nil, cellType: .License),
SettingsRow(title: "Version", subTitle: nil, cellType: .Version)
], title: "Application")
])
}
...

dataSource の宣言部分では、必要なジェネリクスの宣言もすることになります。次に、 UITableViewDataSource の部分です。

UITableViewDataSource

// MARK: UITableViewDataSource

extension SettingsViewController {
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return dataSource.sections.count
}

override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataSource.sections[section].rows.count
}

override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = dataSource.sections[indexPath.section].rows[indexPath.row]
switch row.cellType {
case .Colors:
let cell = tableView.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath)
cell.textLabel?.text = row.title
cell.detailTextLabel?.text = row.subTitle
cell.accessoryType = .DisclosureIndicator
return cell
case .InAppPurchase:
// Return cell
case .Restore:
// Return cell
case .License:
// Return cell
case .Version:
// Return cell
}
}
}

UITableViewCellは複数の種類を分岐で使い分けできるようになっています。ただ、例としては全て分岐していますが、必要に応じてまとめて処理をすることになると思います。次は UITableViewDelegate 周りです。

UITableViewDelegate

// MARK: UITableViewDelegate

extension SettingsViewController {
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let row = dataSource.sections[indexPath.section].rows[indexPath.row]
switch row.cellType {
case .Colors:
// Do something
case .InAppPurchase:
// Do something
case .Restore:
// Do something
case .License:
// Do something
case .Version:
// Do something
}
}

override func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return dataSource.sections[section].title
}
}

セルによってタップの処理を変えています。自由な値を追加できる例として追加していた title をSectionヘッダーに表示してみました。


まとめ

と、こういう形でprotocolに適合させた形で型を書くのは面倒ですが、一度書いてしまえば扱いやすい形になりました。ただ、全てこれでやるというのはあまり考えていなくて、そもそも既存のモデルをTableに表示するなどするときに、 TableViewRowType に準拠させるかというと微妙なところもあると思っています。実際に使えるのは今回の設定のようなその画面に必要な要素をその場で作るという時くらいかもしれません。

また、あえてprotocolに準拠させるのはコスト的に無駄かもしれません。同じ構造の型を直接作れば良い話なので。さらに、あえて型がわかっているならジェネリクスにしなくても良いかもしれません。などなど突っ込みどころ満載ですが、考えた記録として残しておきます...。