Edited at

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

More than 3 years have passed since last update.

昨日、「UITableViewのDataSourceをprotocolで定義しておく - Qiita」という記事を書いたのですが、いろいろ問題点もあってちょっと変更してみました。

やりたいのはよく使うUITableViewのDataSourceを決まり切った形で書くためにprotocol化できないかということです。作りきって公開しているわけではなく考えるために書いています。


更新版

protocol TableViewSectionType {

typealias Row
var rows: [Row] { get set }
}

extension TableViewSectionType {
func title() -> String? {
return nil
}

func headerViewIdentifier() -> String? {
return nil
}
}

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


変更点



  • TableViewRowType というプロトコルがあると、既存のモデルをそれに合わせなければいけなくて、窮屈なのでなくしました。最小単位のアイテムは完全に自由であっても良いかなと。あと、ある程度用意してあると便利なのはSectionまででRowは利用するときはジェネリクスなので自由にやってもいいかなと思いました。

  • ここもうまくいくならもっと充実させれば良いとは思うのですが、TableViewSectionTypeにprotocol extensionを追加しました。使わないならそのままにしておけばいいし、使うときは上書きして使えば良いと思います。optional的に考えています。

DataSourceTypeをインスタンス化してUIViewControllerに持たせ、 UITableViewDataSource に直接セットするというのもあるんですが(Appleの数年前のWWDCビデオでもこのやり方を説明した人のやつがあった気がする。あとサンプルコードもあったはず)、これは昔試して、実際のところはそこで完結しないことが多々あり、複雑になることが多いのでやめています。

ヘッダービューを返すところを、 headerViewIdentifier としているのは、SectionHeaderを保持するのは変な気がしたし、毎回インスタンス化するのもなぁと思って、headerViewはViewControllerが保持して、必要なものををIdentifierで選択して返却すれば良いかなと思ってこうしています。


利用例

enum SettingsCellType {

case Colors
case License
case InAppPurchase
case Restore
case Version
}

struct SettingItem {
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<T>: TableViewSectionType {
var rows: [T]
var title: String?

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

struct SettingsDataSource<T: TableViewSectionType>: TableViewDataSourceType {
var sections: [T]
init(sections: [T]) {
self.sections = sections
}
}

前回とほぼ同じですが、変わったのは下記の点です。


  • 最小単位のアイテムは何のprotocolにも準拠していません。

  • 細いのですが、ジェネリクスをRowやSectionではなくよく使われるTにしました。

  • rowsやsectionsを let ではなく var にしました。これ、変更する場合は、mutatingなfuncが必要になると思います。そういう点では class にした方が良いのかもと思いましたが、それは状況によって使い分ければ良いと思います。

実際にViewControllerの中で使う際の使い方は前回とほとんど同じです。


UIViewController

final class SettingsViewController: UITableViewController {

var dataSource: SettingsDataSource<SettingsSection<SettingItem>>!

override func viewDidLoad() {
super.viewDidLoad()

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

...

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

...

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

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



ViewModelにすればいいじゃんとかRxSwiftやSwiftBond使えばいいじゃんとか

実際仕事だったり、個人で出しているアプリだったりするのはそういうはやりのライブラリを使っています。簡単に書けるのですが、アプリの基本的な部分がロックインされるのもなぁという気持ちもちょっとあって、Appleの提供している範囲でシンプルに書けないかなぁと常々思っていて、こういうことをたまに考えたりしています。

SwiftBondはSwift1.2から2にアップデートするタイミングで大きな仕様変更があり、限られた時間の中でそのコストがかなり大きかったのでそういうところを避けたいというのもあったりします。

ぐだぐだ書きましたが、書くことによって思考が少しでも先に進めば。