RxSwiftでTableViewをハックする🏄
RxSwiftでTableViewに配列を表示する2つの方法についてまとめてみました。
Observableの配列をTableViewにBindする方法
これが多分最も一般的な方法
- ViewModelに定義されている
Observable
の配列をViewController
のTableView
にBind
する方法
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
-
DataSource
をViewController
にネストで作成する。
このDataSource
はRxTableViewDataSourceType
に準拠する必要がある。
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
}
}
-
DataSource
にUITableViewDelegate
も実装する
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
-
ViewModel
のitems
をDataSource
にBind
すし、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をタップなどのアクションに対応する
DataSource
にoutput
のsubject
を用意するだけ!
class DataSource: NSObject, RxTableViewDataSourceType, UITableViewDataSource, UITableViewDelegate {
private let selectedIndexPath = PublishSubject<IndexPath>()
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
selectedIndexPath.onNext(indexPath)
}
}
このsubject
をViewController
でsubscribe
することでタップされたIndexPath
を取得でき、これを使ってごにょごにょできる。
class ViewController: UITableViewController {
private let viewModel = ViewModel()
private let dataSource = DataSource()
override func viewDidLoad() {
super.viewDidLoad()
dataSource.selectedIndexPath
.bindTo(....)
.addDisposableTo(disposeBag)
}
}
配列+その他の値をDataSorceにBindしたい時はどうするのか
-
DataSource
にBind
する場合はObservableの型とRxTableViewDataSourceType
のElement
が同じでなければならない
protocol RxTableViewDataSourceType {
associatedtype Element // これがObservableのTと同じである必要がある。
public func tableView(_ tableView: UITableView, observedEvent: RxSwift.Event<Self.Element>)
}
つまり
このElement
をstruct
などにすることで複数の値をまとめて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
で検索できます。