RxDataSourceを使ってTableViewのHeaderとFooterを生成してみる

概要

前回はRxDataSourceを使って基本的なUITableViewのテンプレートを作ってみました。

RxDataSource

github.com

今回はこのRxDataSourceについて弄ってみてどんな事ができるのかについてより詳しくみていこうと思います。

基本的なstoryboardの配置やソースコードの配置は前回のままですので念の為コピーしたものを載せておきます。

Main.storyboard

f:id:qed805:20190224145732p:plain
rxdatasource

ViewController.swift

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

/// TableViewCellに紐付けるDataModel
struct CustomCellModel {
    var name: String
    var email: String
}

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
}

/// RxDataSourceを使ってDataModelとdataSourceを紐づけるため
extension SectionOfCustomData: SectionModelType {
    // Item に CellのDataModelを紐づける
    typealias Item = CustomCellModel
    
    // ほぼテンプレで可能
    init(original: SectionOfCustomData, items: [Item]) {
        self = original
        self.items = items
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!

    let disposeBag = DisposeBag()
    
    let dataSource = RxTableViewSectionedReloadDataSource<SectionOfCustomData>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item.name
            return cell
    })
    
    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    // SectionOfCustomData の
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")]),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }
}

これが前回までのソースコードとなります。

今回はこれの変更を加えます。

HeaderViewの追加

試しにHeaderViewを追加してみます。RxDataSourceのライブラリのReadmeを確認すると

RxDataSource.swift

/// HeaderViewの作成
dataSource.titleForHeaderInSection = { dataSource, index in
  return dataSource.sectionModels[index].header
}

/// FooterViewの作成
dataSource.titleForFooterInSection = { dataSource, indexPath in
  return dataSource.sectionModels[index].footer
}

このようになっています。 そのため、viewDidLoad()のコードを次のように修正してみます。

ViewController.swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // HeaderViewを追加する
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }

このように修正してアプリをビルドしてみます。

f:id:qed805:20190224150449p:plain
tableView_headerview

このように無事にヘッダーが表示されたら成功です。

FooterViewの追加

では、次にCustomなFooterViewの追加を実装してみます。

まずはSectionOfCustomDataを次のように修正してみます。

ViewController.swift

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
    /// セクションフッターの名前
    var footer: String
}

セクションフッターの名前を追加しました。

これによって変数sectionsの修正が必要になります。

ViewController.swift

    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")],
                            footer: "1st section footer"),  // footer: を追加しました
    ]

これによってsectionsのデータ構造が変わってfooterも追加されます。
ですが、これだけではUITableViewには反映されません。

最後にviewDidLoad()をこのように修正します。

ViewController.swift

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // HeaderViewを追加する
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        // FooterViewを追加する
        dataSource.titleForFooterInSection = { dataSource, index in
            return dataSource.sectionModels[index].footer
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }

これでアプリをビルドしてみましょう。

次のようにfooterが追加表示されていたら成功です。

f:id:qed805:20190224152517p:plain
tableView_footer

Footerが表示されるようになりました。

このようにRxDataSourceを上手く使えば普通のUITableViewのようにデータを扱えるようになります。

Qiitaの記事ではRxDataSourceの使い方よりもそれを意識した設計方針の方が難しく感じますね。

RxDataSourceに苦手意識を持っている僕のような方であれば今回の記事を参考にして最低限の実装方法を理解すれば自ずと 設計方針が見えてくると思います。

とりあえず、これでUITableViewの実装は一通り経験できたので頑張れそうだと思います。

全体のソースコード

一応、念の為、全体のソースコードを乗せておきます。

ViewController.swift

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

/// TableViewCellに紐付けるDataModel
struct CustomCellModel {
    var name: String
    var email: String
}

/// セクションヘッダーの名前とセクション内のitem
struct SectionOfCustomData {
    /// セクションヘッダーの名前
    var header: String
    /// indexPath.row のcellのデータ
    var items: [Item]
    /// セクションフッターの名前
    var footer: String
}

/// RxDataSourceを使ってDataModelとdataSourceを紐づけるため
extension SectionOfCustomData: SectionModelType {
    // Item に CellのDataModelを紐づける
    typealias Item = CustomCellModel
    
    // ほぼテンプレで可能
    init(original: SectionOfCustomData, items: [Item]) {
        self = original
        self.items = items
    }
}

class ViewController: UIViewController {
    
    @IBOutlet weak var tableView: UITableView!

    let disposeBag = DisposeBag()
    
    let dataSource = RxTableViewSectionedReloadDataSource<SectionOfCustomData>(
        configureCell: { dataSource, tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
            cell.textLabel?.text = item.name
            return cell
    })
    
    // 複数のsectionを表示させたい場合はSectionOfCustomDataを生成していきます。
    let sections = [
        SectionOfCustomData(header: "1st section",
                            items: [CustomCellModel(name: "山田花子", email: "hanako@gmail.com"),
                                    CustomCellModel(name: "田中太郎", email: "taro@gmail.com"),
                                    CustomCellModel(name: "石田真一", email: "shinichi@gmail.com")],
                            footer: "1st section footer"),
    ]

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // HeaderViewを追加する
        dataSource.titleForHeaderInSection = { dataSource, index in
            return dataSource.sectionModels[index].header
        }
        
        // FooterViewを追加する
        dataSource.titleForFooterInSection = { dataSource, index in
            return dataSource.sectionModels[index].footer
        }
        
        Observable.just(sections)  // sectionsを生成して
            .bind(to: tableView.rx.items(dataSource: dataSource))  // itemsのdataSourceに紐づける
            .disposed(by: disposeBag)
    }
}

なんやかんやでちょっとずつ複雑になってきましたね。