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

February 04, 2016

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

やりたいのはよく使う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: TableViewSectionType { var rows: [T] var title: String?

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

}

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

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

  • 最小単位のアイテムは何のprotocolにも準拠していません。
  • 細いのですが、ジェネリクスをRowやSectionではなくよく使われるTにしました。

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

final class SettingsViewController: UITableViewController {

var dataSource: SettingsDataSource\>!

override func viewDidLoad() {
    super.viewDidLoad()
            
    dataSource = SettingsDataSource\>(sections: \[
        SettingsSection(rows: \[
            SettingItem(title: "Colors", subTitle: nil, cellType: .Colors)
            \], title: "General"),
        SettingsSection(rows: \[
            SettingItem(title: "InAppPurchase", subTitle: nil, cellType: .InAppPurchase),
            SettingItem(title: "Restore", subTitle: nil, cellType: .Restore)
            \], title: "Purchase"),
        SettingsSection(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にアップデートするタイミングで大きな仕様変更があり、限られた時間の中でそのコストがかなり大きかったのでそういうところを避けたいというのもあったりします。

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


Profile picture

Written by morizotter who lives and works in Tokyo building useful things. You should follow them on Twitter