LoginSignup
101
81

More than 5 years have passed since last update.

RxSwiftでTableViewをハックする🏄

Posted at

RxSwiftでTableViewをハックする🏄

RxSwiftでTableViewに配列を表示する2つの方法についてまとめてみました。

Observableの配列をTableViewにBindする方法

これが多分最も一般的な方法

  • ViewModelに定義されているObservableの配列をViewControllerTableViewBindする方法
class ViewModel {

    var items: Observable<Item> {
        return Observable.of(Item.dummyData)
    }
}
class ViewController: UITableViewController {

    private let viewModel = ViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.items
            .bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: ItemListViewCell.self)) { index, item, cell in
                cell.item = item
            }
            .addDisposableTo(disposeBag)
    }
}

TableViewのSectionを分けたりHeaderViewを設定したりする方法

これがあまり使われていない方法

Step 1

  • ViewModelを作成する
class ViewModel {

    var elements: Observable<Item> {
        return Observable.of(Item.dummyData)
    }
}

Step 2

  • DataSourceViewControllerにネストで作成する。
このDataSourceRxTableViewDataSourceTypeに準拠する必要がある。
protocol RxTableViewDataSourceType {

    associatedtype Element
    public func tableView(_ tableView: UITableView, observedEvent: RxSwift.Event<Self.Element>)

}
  • DataSourceを作成する
class DataSource: NSObject, RxTableViewDataSourceType, UITableViewDataSource {

    typealias Element = [[Item]]

    var items: Element = [[]]

    func tableView(_ tableView: UITableView, observedEvent: Event<Array<[Item]>>) {
        if case .next(let items) = observedEvent {
            self.items = items
            tableView.reloadData()
        }
    }

    // MARK: - UITableViewDataSource

    func numberOfSections(in tableView: UITableView) -> Int {
        return items.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items[section].count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") as! ItemListViewCell
        let item = items[indexPath.section][indexPath.row]
        cell.item = item
        return cell
    }
}
  • DataSourceUITableViewDelegateも実装する

class DataSource: NSObject, RxTableViewDataSourceType, UITableViewDataSource, UITableViewDelegate {

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let headerView = tableView.dequeueReusableCell(withIdentifier: "Header") as! ItemHeaderViewCell
        return headerView
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 120
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 64
    }
}

Step 3

  • ViewModelitemsDataSourceBindすし、delegateをセットする
class ViewController: UITableViewController {
    private let viewModel = ViewModel()
    private let dataSource = DataSource()

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.items
            .bindTo(tableView.rx.items(dataSource: dataSource))
            .addDisposableTo(disposeBag)

        tableView.delegate = dataSource
    }
}

この3 StepでViewModel側で扱っている配列をSectionやHeaderViewを差し込みつつTableViewにバインドできる。

DataSourceを使った場合TableViewをタップなどのアクションに対応する

DataSourceoutputsubjectを用意するだけ!

class DataSource: NSObject, RxTableViewDataSourceType, UITableViewDataSource, UITableViewDelegate {

    private let selectedIndexPath = PublishSubject<IndexPath>()

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        selectedIndexPath.onNext(indexPath)
    }
}

このsubjectViewControllersubscribeすることでタップされたIndexPathを取得でき、これを使ってごにょごにょできる。

class ViewController: UITableViewController {

    private let viewModel = ViewModel()
    private let dataSource = DataSource()

    override func viewDidLoad() {
        super.viewDidLoad()

        dataSource.selectedIndexPath
            .bindTo(....)
            .addDisposableTo(disposeBag)
    }
}

配列+その他の値をDataSorceにBindしたい時はどうするのか

  • DataSourceBindする場合はObservableの型とRxTableViewDataSourceTypeElementが同じでなければならない
protocol RxTableViewDataSourceType {

    associatedtype Element // これがObservableのTと同じである必要がある。
    public func tableView(_ tableView: UITableView, observedEvent: RxSwift.Event<Self.Element>)

}

つまり

このElementstructなどにすることで複数の値をまとめてBindすることができる。

associatedTypeはenumやstructにすることも可能です。
class DataSource: NSObject, RxTableViewDataSourceType, UITableViewDataSource {

    struct Element {
        let items: [[Item]]
        let user: User
    }

    var items: [Item] = [[]]
    var user: User? = nil

    func tableView(_ tableView: UITableView, observedEvent: Event<Array<[Item]>>) {
        if case .next(let element) = observedEvent {
            items = element.items
            user = element.user
            tableView.reloadData()
        }
    }
}

あとはViewControllerでstruct Elementを作りBindするだけ!


class ViewController: UITableViewController {

    private let viewModel = ViewModel()
    private let dataSource = DataSource()

    override func viewDidLoad() {
        super.viewDidLoad()

        Observable
            .combineLatest(viewModel.user, viewModel.items) { DataSource.Element(items: $0, user: $1) }
            .bindTo(tableView.rx.items(dataSource: dataSource))
            .addDisposableTo(disposeBag)
    }
}

最後に

このTableViewでできることは全部CollectionViewでも実装可能です!

あと日本語版のRxSwiftが学習できるサンプルアプリ(or 質問とかできる場所)を作りたいと思ってるのですが、どなたか興味があればご連絡ください。全てのSNSはyuzushiohで検索できます。

101
81
16

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
101
81