24
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-02-03

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

24
25
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
25

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?