iOS
UITableView
MVVM
RxSwift

iOS × RxSwift × MVVM:UITableView基本実装

[目次] iOS × RxSwift × MVVM

スクリーンショット 2019-01-14 17.29.42.png


概要

「iOS × RxSwift × MVVM」観点でのUITableView基本実装パターンまとめ。

なお、サンプルコードはiOS × RxSwift × MVVM:UIViewController再利用実装をベースにしています。


0,基本実装パターン一覧

No.
項目
方法概要
メリット
デメリット
利用場面
備考

1
UITableViewDataSource/UITabelViewDelegate
標準のDelegateインスタンスをセットする方法
UIKitの標準的実装方法の拡張であるため、学習コストが少なくて済む。
タイミングに応じてreloadData()等を記述する必要がある。ロジック実装箇所とViewController参照可能箇所が必ずしも一致しないため、実装が逆に複雑になる場合がある。
なし🙅
(参考程度)

2
RxCocoa標準記法
データ配列のObservableにBindする形で記述する方法。
記法さえ慣れてしまえば実装は比較的容易。また、ロジック実装箇所とViewController参照可能箇所を一致させられるため、実装が複雑になることを避けられる。
単一セクション・単一種類セルを前提にしているため、実装できないパターンがある。
単一セクションかつ単一種類セル

3
RxTableViewSectionedReloadDataSource
2の方法の拡張版。
複数セクション・複数種類セルにも対応可能。
RxDataSourcesを入れることが前提。データが変わる際に呼ばれるのが「全セル更新」であるため、細かな制御はできない。
複数セクションor複数種類セル

4
RxTableViewSectionedAnimatedDataSource
3の方法の拡張版。
3に加えて、挿入・削除・一部セル更新などの細かな制御に対応する。
RxDataSourcesを入れることが前提。データソースをProtocolに合わせるコストが地味に手間。
セル更新が複雑


1,UITableViewDataSource/UITabelViewDelegate


Sample①:UITableViewDataSource/UITabelViewDelegate

// MARK: ViewModel

extension HogeTableViewController.ViewModel {
final class Default: HogeTableViewController.ViewModel {
// MARK: Property
let tableDataBehaviorRelay = BehaviorRelay<[HogeCellModel]>(value: [])

// MARK: Function
override func bindTo(_ viewController: HogeTableViewController) -> Disposable {
// generates disposables
var disposables = [super.bindTo(viewController)]

// ViewModel -> ViewController
disposables += [viewController.tableView.rx.setDataSource(self)]
disposables += [viewController.tableView.rx.setDelegate(self)]

// returns disposable
return Disposables.create(disposables)
}
}
}

// MARK: UITableViewDataSource
extension HogeTableViewController.ViewModel.Default: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.tableDataBehaviorRelay.value.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let hogeCell = tableView.dequeueReusableCell(withIdentifier: "HogeCell", for: indexPath)
as? HogeTableViewCell else {
return UITableViewCell()
}
let data = self.tableDataBehaviorRelay.value[indexPath.row]
hogeCell.titleLabel.text = data.titleNumber?.description
hogeCell.detailLabel.text = data.detailDate?.description
return hogeCell
}
}

// MARK: UITableViewDataSource
extension HogeTableViewController.ViewModel.Default: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("selected: \(indexPath.row)")
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
print("willDisplay: \(indexPath.row)")
}
}



概要

UIKit標準DelegateProtocolをViewModelに適応させた上で、これをViewControllerにセットする方法。


利用場面

なし🙅

MVVM前提とした場合、ロジック実装箇所とViewController参照可能箇所が必ずしも一致しないため、実装で難儀する可能性大。


使い方


  1. ViewModelをUITableViewDataSource,UITableViewDelegateに準拠させる。


  2. aTableView.rx.setDataSource(self),aTableView.rx.setDelegate(self)の形でDelegateセットする。


2,RxCocoa標準記法


Sample②:RxCocoa標準記法

// MARK: ViewModel

extension HogeTableViewController.ViewModel {
final class Default: HogeTableViewController.ViewModel {
// MARK: Property
let tableDataBehaviorRelay = BehaviorRelay<[HogeCellModel]>(value: [])

// MARK: Function
override func bindTo(_ viewController: HogeTableViewController) -> Disposable {
// generates disposables
var disposables = [super.bindTo(viewController)]

// ViewModel -> ViewController
disposables += [self.tableDataBehaviorRelay
.bind(to: viewController.tableView.rx.items(cellIdentifier: "HogeCell", cellType: HogeTableViewCell.self)) { row, data, cell in
cell.titleLabel.text = data.titleNumber?.description
cell.detailLabel.text = data.detailDate?.description
}]

// ViewController -> ViewModel
disposables += [viewController.tableView.rx.itemSelected
.subscribe(onNext: { indexPath in
print("selected: \(indexPath.row)")
})]
disposables += [viewController.tableView.rx.willDisplayCell
.subscribe(onNext: { cell, indexPath in
print("willDisplay: \(indexPath.row)")
})]

// returns disposable
return Disposables.create(disposables)
}
}
}



概要

データ配列のObservableにBindする形で記述する方法。RxSwiftでUITableViewを記述する際の基本型。


利用場面


  • 単一セクション

  • 単一種類セル

  • 複雑なセル更新がかからない(※下からセルが足されていくものは除く)


使い方


  1. セル用データモデルを用意する

  2. セルデータ配列Observable系プロパティ(特に問題なければBehaviorRelay<>型で良い)を用意する。

  3. 3のObservable系プロパティに.bind(to: viewController.tableView.rx.items(cellIdentifier:, cellType:)) { ~ }を書く。

  4. 4の~の部分にセルの設定に関する記述を書く。

  5. UITableViewDelegateに関する記述は、aTableView.rx.~でObservableにアクセスして記述。


3,RxTableViewSectionedReloadDataSource


Sample③:RxTableViewSectionedReloadDataSource

import RxDataSources

// MARK: Declaration
struct HogeCellModel {
var titleNumber: Int?
var detailDate: Date?
}

struct HogeSectionModel: SectionModelType {
typealias Item = HogeCellModel

var sectionTitle: String? = nil
var items: [Item] = []

init(original: HogeSectionModel, items: [Item]) {
self = original
self.items = items
}
}

// MARK: ViewModel
extension HogeTableViewController.ViewModel {
final class Default: HogeTableViewController.ViewModel {
// MARK: Property
let tableDataBehaviorRelay = BehaviorRelay<[HogeSectionModel]>(value: [])

// MARK: Function
override func bindTo(_ viewController: HogeTableViewController) -> Disposable {
// generates disposables
var disposables = [super.bindTo(viewController)]

// ViewModel -> ViewController
disposables += [self.tableDataBehaviorRelay
.bind(to: viewController.tableView.rx.items(dataSource: RxTableViewSectionedReloadDataSource<HogeSectionModel>(
configureCell: { dataSeource, tableView, indexPath, cellData in
guard let hogeCell = tableView.dequeueReusableCell(withIdentifier: "HogeCell", for: indexPath)
as? HogeTableViewCell else {
return UITableViewCell()
}
hogeCell.titleLabel.text = cellData.titleNumber?.description
hogeCell.detailLabel.text = cellData.detailDate?.description
return hogeCell
},
titleForHeaderInSection: { dataSource, section in
return dataSource.sectionModels[section].sectionTitle
})))]

// ViewController -> ViewModel
disposables += [viewController.tableView.rx.itemSelected
.subscribe(onNext: { indexPath in
print("selected: \(indexPath.row)")
})]
disposables += [viewController.tableView.rx.willDisplayCell
.subscribe(onNext: { cell, indexPath in
print("willDisplay: \(indexPath.row)")
})]

// returns disposable
return Disposables.create(disposables)
}
}
}



概要

RxCocoa標準記法の拡張版のひとつで、RxSwiftCommunity/RxDataSourcesをインポートして使う記法。RxCocoa標準記法に加えて、復数セクション・復数種類セルに対応している。ただし、実装は2に比べると手間なので、可能ならそちらを使ったほうが良い。


利用場面


  • 複数セクション複数

  • 複数種類セル

  • 複雑なセル更新がかからない(※下からセルが足されていくものは除く)


使い方


  1. セル用、セクション用のデータモデルを用意する

  2. セクション用のデータモデルをSectionModelTypeに準拠させる。(これによりRxDatasourcesがセクション内のセルデータ型を認識できるようになる)

  3. セクションデータ配列Observable系プロパティ(特に問題なければBehaviorRelay<>型で良い)を用意する。

  4. 3のObservable系プロパティに.bind(to: viewController.tableView.rx.items(dataSource: ~))を書く。

  5. 4の~の部分にRxTableViewSectionedReloadDataSource<セクションデータ型>インスタンスを書く。(ここでセルやセクションについての記述をする)

  6. UITableViewDelegateに関する記述は、aTableView.rx.~でObservableにアクセスして記述。


4,RxTableViewSectionedAnimatedDataSource


Sample④:RxTableViewSectionedAnimatedDataSource

import RxDataSources

// MARK: Declaration
struct HogeCellModel: IdentifiableType, Equatable {
typealias Identity = Int?

var titleNumber: Int?
var detailDate: Date?
var identity: Identity {
return self.titleNumber
}
}

struct HogeSectionModel: AnimatableSectionModelType {
typealias Identity = String?
typealias Item = HogeCellModel

var sectionTitle: String? = nil
var items: [Item] = []
var identity: Identity {
return self.sectionTitle
}

init(original: HogeSectionModel, items: [Item]) {
self = original
self.items = items
}
}

// MARK: ViewModel
extension HogeTableViewController.ViewModel {
final class Default: HogeTableViewController.ViewModel {
// MARK: Property
let tableDataBehaviorRelay = BehaviorRelay<[HogeSectionModel]>(value: [])

// MARK: Function
override func bindTo(_ viewController: HogeTableViewController) -> Disposable {
// generates disposables
var disposables = [super.bindTo(viewController)]

// ViewModel -> ViewController
disposables += [self.tableDataBehaviorRelay
.bind(to: viewController.tableView.rx.items(dataSource: RxTableViewSectionedAnimatedDataSource<HogeSectionModel>(
configureCell: { dataSeource, tableView, indexPath, cellData in
guard let hogeCell = tableView.dequeueReusableCell(withIdentifier: "HogeCell", for: indexPath)
as? HogeTableViewCell else {
return UITableViewCell()
}
hogeCell.titleLabel.text = cellData.titleNumber?.description
hogeCell.detailLabel.text = cellData.detailDate?.description
return hogeCell
},
titleForHeaderInSection: { dataSource, section in
return dataSource.sectionModels[section].sectionTitle
})))]

// ViewController -> ViewModel
disposables += [viewController.tableView.rx.itemSelected
.subscribe(onNext: { indexPath in
print("selected: \(indexPath.row)")
})]
disposables += [viewController.tableView.rx.willDisplayCell
.subscribe(onNext: { cell, indexPath in
print("willDisplay: \(indexPath.row)")
})]

// returns disposable
return Disposables.create(disposables)
}
}
}



概要

RxCocoa標準記法の拡張版のひとつで、3と同じくRxSwiftCommunity/RxDataSourcesをインポートして使う記法。3に加えて、挿入・削除・一部セル更新などの細かな制御に対応している。ただし、実装は2、3に比べると手間なので、可能ならそれらを使ったほうが良い。


利用場面


  • 複数セクション

  • 複数種類セル

  • 挿入・削除・一部セル更新等の複雑なセル更新を行なう


使い方


  1. セル用、セクション用のデータモデルを用意する

  2. セル用のデータモデルをIdentifiableType Equatableに準拠させる。(これによりRxDatasourcesが各セルデータの区別をつけられるようになる)

  3. セクション用のデータモデルをAnimatableSectionModelTypeに準拠させる。(これによりRxDatasourcesがセクションの区別およびセクション内のセルデータ型を認識できるようになる)

  4. セクションデータ配列Observable系プロパティ(特に問題なければBehaviorRelay<>型で良い)を用意する。

  5. 4のObservable系プロパティに.bind(to: viewController.tableView.rx.items(dataSource: ~))を書く。

  6. 5の~の部分にRxTableViewSectionedAnimatedDataSource<セクションデータ型>インスタンスを書く。(ここでセルやセクションについてのアニメーションを含む記述をする)

  7. UITableViewDelegateに関する記述は、aTableView.rx.~でObservableにアクセスして記述。


Tips🌞


🌞1: たまにセルが消える、更新されない

UIKit標準の方法でもあることだが、データソースの更新はメインスレッドで行われる必要があり、サブスレッド等で更新をするとTableViewの更新が行われない場合がある。

BehaviorRelay等のデータソースの更新の際に、下記のような形で囲んでみて試してみる。というか、メインスレッドで行われない可能性のある箇所では、基本的にコレで囲うのが無難。


たまにセルが消える件の解消法

Dispatch.main.async {

tableDataBehaviorRelay.accept([data0, data1, data2, data3])
}


備考

(特になし)