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

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

Pocket
LINEで送る

You may also like...