UITableViewのDataSourceをprotocolで定義しておく
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はとりあえず、 title
と subtitle
と cellType
を持たせました。 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に準拠させるのはコスト的に無駄かもしれません。同じ構造の型を直接作れば良い話なので。さらに、あえて型がわかっているならジェネリクスにしなくても良いかもしれません。などなど突っ込みどころ満載ですが、考えた記録として残しておきます…。