前言
RxSwift的魅力想必用過的人都有心得體會,簡直就是從入門到想放棄,從想放棄到愛不釋手的過程。但是RxSwift的前世今生并不是本文想寫的內(nèi)容,而是其中很常用又很重要的一個部分UITableView。不管用什么語言開發(fā)移動端App,新人們基本都會被告知,掌握了TableView和CollectionView,就學(xué)會了這門語言的80%,可想而知其重要性。
RxSwift的Git社區(qū),其中一個庫RxDataSources是本文的重頭戲。UITableViewDataSource和UITableViewDelegate是UITableView兩個重要的代理,RxDataSources用RxSwift封裝了tableView(:cellForRowAt)和tableView(:didSelectRowAt)方法,后面會細(xì)細(xì)道來。
正文
本文將從一個最最簡單的TableView例子講起,并將之用RxSwift實現(xiàn),然后一步步將其功能完善。當(dāng)然靈感都是來自于RxSwift官方Demo,demo中有簡單的RxTableView(注:基于RxSwift的UITableView,筆者稱之為RxTableView)例子。
第一個VC:Simplest UITableView
UITableViewDataSource和UITableViewDelegate是UITableView的核心,簡單說明下幾個最重要的代理方法:
// MARK: - UITableViewDataSource
override func numberOfSections(in tableView: UITableView) -> Int {
return 1 // Section的數(shù)量
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count // Section中Row的數(shù)量
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath) // TableViewCell繪制
cell.textLabel?.text = items[indexPath.row]
return cell
}
// MARK: - UITableViewDelegate
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true) // TableViewCell點擊事件
let viewController = RxTableViewController()
viewController.type = RxTableViewType(rawValue: indexPath.row)!
self.navigationController?.pushViewController(viewController, animated: true)
}
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 40 // TableViewCell高度
}
第二個VC:Simplest RXTableView
RxDataSources庫把UITableViewDataSource和UITableViewDelegate的代理封裝成了響應(yīng)式編程的風(fēng)格,在代碼量上少了很多,當(dāng)然也更難理解:
// 1.將數(shù)據(jù)綁定到TableView上
let items = Observable.just((0..<30).map({ "\($0)"}))
items.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
cell.textLabel?.text = "row \(element)"
}.disposed(by: disposeBag)
// 2.TableViewCell點擊事件響應(yīng)
tableView.rx.modelSelected(String.self).subscribe(onNext: { (item) in
print(item) // "1"
}).disposed(by: disposeBag)
幾行代碼就可以實現(xiàn)cell的繪制和點擊處理,被觀察者Obserable有一個bind(to:)方法,將自己與訂閱者Observer綁定,在Obserable發(fā)送事件的時候,Observer會同步更新,兩者的數(shù)據(jù)類型必須保持一致。例如Obserable<String>類型的變量str,調(diào)用bind(to:)方法將其綁定到一個UILabel類型的變量label的text屬性上(同為String類型),那么str變化的同時, label.text的值跟str保持一致。
var str = "str"
let label = UILabel(frame: .zero)
Observable.of(str).bind(to: label.rx.text).disposed(by: disposeBag) // label.text = "str"
str = "changed" // label.text = "changed"
實質(zhì)上,bind(to:)是對subsribe做了一層封裝,subscribe(onNext:)同樣可以實現(xiàn)上述功能:
Observable.of(str).subscribe(onNext: { label.text = $0 }).disposed(by: disposeBag)
明顯,bind(to:)比subscribe(onNext:)更加"響應(yīng)式"。
第三個VC:RXTableView of Sections
用items(cellIdentifier:cellType:)方法可以滿足單個Section的場景,它還有另外一個方法items(dataSource:),則可以滿足多個Section的場景,當(dāng)然步驟也更加復(fù)雜。
// 自定義Model,遵循SectionModelType
struct SectionModel<HeaderType, ItemType>: SectionModelType {
var header: HeaderType
var items: [ItemType]
init(header: HeaderType, items: [ItemType]) {
self.header = header
self.items = items
}
init(original: SectionModel<HeaderType, ItemType>, items: [ItemType]) {
self.header = original.header
self.items = items
}
}
// 1.創(chuàng)建DataSource
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String,Int>>(configureCell: { (section, tableView, indexPath, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "row \(element)"
return cell
})
// 2.設(shè)置HeaderTitle(可選)
dataSource.titleForHeaderInSection = { (dataSource, sectionIndex) -> String? in
return dataSource[sectionIndex].header
}
// 3.將數(shù)據(jù)綁定到TableView上
let items = [SectionModel(header: "section 1", items: [1, 2, 3]), SectionModel(header: "section 2", items: [1, 2, 3])]
Observable.just(items).bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
// 4.TableViewCell點擊事件響應(yīng)
tableView.rx.itemSelected.map {
return ($0, items[$0.section].header, dataSource[$0])
}.subscribe(onNext: { indexPath, header, item in
print("\(header), row \(item)") // "section 1, row 1"
}).disposed(by: disposeBag)
相對于上一個VC的直接綁定,這里需要額外定義Model,創(chuàng)建DataSource,并調(diào)用tableView.rx.items(dataSource:)方法來進(jìn)行綁定,這些額外的步驟都是為了提高可定制化的程度。這里Cell的點擊事件響應(yīng)已經(jīng)由modelSelected()方法換成了itemSelected()方法,差別就是modelSelected()方法會直接返回選中的model,而不會返回選中model的indexPath。
RxDataSources的優(yōu)劣
RxDataSources庫已經(jīng)提供了強大的功能來滿足RxSwift開發(fā)者們實現(xiàn)響應(yīng)式TableView的迫切愿望,但是也有不完美的地方,筆者將結(jié)合自身經(jīng)歷列出優(yōu)勢和不足:
優(yōu)勢
- 與RxSwift結(jié)合,讓代碼更加“響應(yīng)式”。即使用復(fù)寫delegate方法來實現(xiàn)UITableViewDataSource和UITableViewDelegate也是完全OK的,只是面向?qū)ο缶幊膛c響應(yīng)式編程的代碼糅合在一起,顯示特別奇怪。一切皆“響應(yīng)”,沒有什么是響應(yīng)式編程無法實現(xiàn)的,特別不能用面向?qū)ο蟮乃季S去思考,會越走越遠(yuǎn)。
- 類似閉包語法的風(fēng)格,讓代碼更加簡潔。以前筆者喜歡用delegate,覺得邏輯很簡單直觀,但是久了就會發(fā)現(xiàn)調(diào)試起來,頁面上下滾動太頻繁,對象的創(chuàng)建和代理方法調(diào)用基本不在同一頁面。RxDataSources這種“閉包式”的語法,讓代碼變得更加簡潔,更易調(diào)試。
- 其他優(yōu)點應(yīng)該還有很多。。。。。
不足
尚未完全Rx化,不得不復(fù)寫代理方法。筆者用了一段時間這個庫,目前發(fā)現(xiàn)有兩點是無法做到響應(yīng)式實現(xiàn)
- 通常在調(diào)用tableView(:didSelectRowAt:)方法后,會調(diào)用deselectRow(at:animated:) 函數(shù)來取消選中,但是目前沒有發(fā)現(xiàn)調(diào)用modelSelected()或itemSelected()方法后有什么方法可以直接取消選中,而不是在subscribe(onNext:)方法中用面向?qū)ο蟮倪壿媽崿F(xiàn);
- tableView(: heightForRowAt: )方法是用來設(shè)置高度的,RxTableViewSectionedReloadDataSource的配置項中目前沒有發(fā)現(xiàn)有什么方法可以設(shè)置Cell高度。
以上都是個人觀點,假如是因為筆者才疏學(xué)淺才沒有發(fā)現(xiàn)的話,還望指正。
總結(jié)
RxTableView的入門講解就告一段落,后續(xù)會出一篇進(jìn)階篇,深入分析數(shù)據(jù)和視圖的綁定機制,并實現(xiàn)一(數(shù)據(jù))對多(視圖)的綁定,一(數(shù)據(jù))對一(多視圖切換)的綁定,也是自己在實際項目中遇到的坑,拿出來跟各位共同探討。
Demo地址:https://github.com/MrSuperJJ/RxTableViewDemo