LoginSignup
15
16

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-02-04

昨日、「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にアップデートするタイミングで大きな仕様変更があり、限られた時間の中でそのコストがかなり大きかったのでそういうところを避けたいというのもあったりします。

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

15
16
1

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
15
16