復雜UITableview綁定Rx實現(xiàn)
RxCocoa沒有實現(xiàn)復雜UITableview數(shù)據(jù)綁定(如多組數(shù)據(jù)莱革、cell編輯等),需要自行實現(xiàn),不過通過對RxCocoa中UITableview單組數(shù)據(jù)綁定的分析,其實實現(xiàn)思路是一樣的。
定義一個SectionModelType
協(xié)議來規(guī)范整個組的數(shù)據(jù):
protocol SectionModelType {
associatedtype Section
associatedtype Item
var model: Section { get }
var items: [Item] { get }
init(model: Section, items: [Item])
}
- 定義了兩個關聯(lián)類型
Section
燃辖,Item
表示組數(shù)據(jù)和組中的行數(shù)據(jù) - 定義兩個屬性
model
,items
存儲組數(shù)據(jù)和組中的行數(shù)據(jù)
定義一個SectionModelType
類來存儲、關聯(lián)源數(shù)據(jù)的數(shù)據(jù):
class SectionedDataSource<Section: NSObject, SectionModelType>: SectionedViewDataSourceType {
private var _sectionModels: [Section] = []
func setSections(_ sections: [Section]) {
_sectionModels = sections
}
func sectionsCount() -> Int {
return _sectionModels.count
}
func itemsCount(section: Int) -> Int {
return _sectionModels[section].items.count
}
subscript(section: Int) -> Section {
let sectionModel = _sectionModels[section]
return Section(model: sectionModel.model, items: sectionModel.items)
}
subscript(indexPath: IndexPath) -> Section.Item {
return _sectionModels[indexPath.section].items[indexPath.row]
}
// MARK: SectionedViewDataSourceType
func model(at indexPath: IndexPath) throws -> Any { self[indexPath] }
}
- 該類遵守
SectionedViewDataSourceType
協(xié)議來規(guī)定如何獲取數(shù)據(jù) - 該類的本質(zhì)存儲組數(shù)據(jù)數(shù)組拂酣,并且定義了一些簡便的函數(shù)來獲取數(shù)據(jù)
定義一個TableViewSectionedDataSource
類繼承自SectionedDataSource
,遵守UITableViewDataSource
仲义、RxTableViewDataSourceType協(xié)議
:
class TableViewSectionedDataSource<Section: SectionModelType>: SectionedDataSource<Section>, UITableViewDataSource, RxTableViewDataSourceType {
typealias CellForRow = (TableViewSectionedDataSource<Section>, UITableView, IndexPath) -> UITableViewCell
typealias TitleForHeader = (TableViewSectionedDataSource<Section>, UITableView, Int) -> String?
typealias TitleForFooter = (TableViewSectionedDataSource<Section>, UITableView, Int) -> String?
typealias CanEditRow = (TableViewSectionedDataSource<Section>, UITableView, IndexPath) -> Bool
typealias CanMoveRow = (TableViewSectionedDataSource<Section>, UITableView, IndexPath) -> Bool
var cellForRow: CellForRow
var titleForHeader: TitleForHeader
var canEditRow: CanEditRow
var canMoveRow: CanMoveRow
init(cellForRow: @escaping CellForRow, titleForHeader: @escaping TitleForHeader = { _,_,_ in nil }, canEditRow: @escaping CanEditRow = { _,_,_ in false }, canMoveRow: @escaping CanMoveRow = { _,_,_ in false }) {
self.cellForRow = cellForRow
self.titleForHeader = titleForHeader
self.canEditRow = canEditRow
self.canMoveRow = canMoveRow
super.init()
}
// MARK: UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int { sectionsCount() }
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { itemsCount(section: section) }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { cellForRow(self, tableView, indexPath) }
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { titleForHeader(self, tableView, section) }
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { nil }
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { canEditRow(self, tableView, indexPath) }
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { canMoveRow(self, tableView, indexPath) }
// MArK: RxTableViewDataSourceType
typealias Element = [Section]
func tableView(_ tableView: UITableView, observedEvent: Event<TableViewSectionedDataSource<Section>.Element>) {
Binder(self) { (dataSource, element: Element) in
dataSource.setSections(element)
tableView.reloadData()
}.on(observedEvent)
}
}
- 類繼承
SectionedDataSource
是達到對原數(shù)據(jù)的取用 - 遵守
UITableViewDataSource
協(xié)議為UITableview
提供數(shù)據(jù) - 遵守
RxTableViewDataSourceType
協(xié)議實現(xiàn)對UITableview的datasource
代理所需數(shù)據(jù)存儲婶熬,并刷新列表 -
cellForRow
剑勾、titleForHeader
、canEditRow
赵颅、canMoveRow
這幾個屬性分別存儲將原數(shù)據(jù)轉(zhuǎn)化為UITableViewDataSource
協(xié)議所需要數(shù)據(jù)的閉包虽另,既是行的cell、組頭的標題饺谬、行能否編輯捂刺、行能否移動
多組數(shù)據(jù)綁定
新建控制器,構(gòu)建一個UITableview作為屬性tableView
募寨。
定義SectionModel
遵守SectionModelType
表示組數(shù)據(jù):
struct SectionModel<SectionType, ItemType>: SectionModelType {
typealias Section = SectionType
typealias Item = ItemType
var model: Section
var items: [Item]
init(model: Section, items: [Item]) {
self.model = model
self.items = items
}
}
構(gòu)建數(shù)據(jù)序列綁定到tableView
:
let observable = Observable.just([
SectionModel(model: 1, items: Array(1...10)),
SectionModel(model: 2, items: Array(1...10)),
SectionModel(model: 3, items: Array(1...10)),
SectionModel(model: 4, items: Array(1...10)),
SectionModel(model: 5, items: Array(1...10)),
SectionModel(model: 6, items: Array(1...10)),
SectionModel(model: 7, items: Array(1...10)),
SectionModel(model: 8, items: Array(1...10)),
SectionModel(model: 9, items: Array(1...10)),
SectionModel(model: 10, items: Array(1...10))
])
let dataSource = TableViewSectionedDataSource<SectionModel<Int, Int>>(cellForRow: { (dataSource, tableView, indexPath) -> UITableViewCell in
let cell = CommonCell.cellFor(tableView: tableView)
let item = dataSource[indexPath]
cell.textLabel?.text = "我是(\(indexPath.section), \(indexPath.row)), \(item)"
return cell
}, titleForHeader: { "第\($2)組我是\($0[$2].model)" })
observable.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
可編輯的UITableView綁定
新建一個控制器族展,構(gòu)建一個UITableview作為屬性tableView。
在導航欄右側(cè)設置編輯item
:
self.navigationItem.rightBarButtonItem = self.editButtonItem
self.navigationItem.rightBarButtonItem?.title = "編輯"
重寫控制器的setEditing
函數(shù)來編輯UITableview:
override func setEditing(_ editing: Bool, animated: Bool) {
super.setEditing(editing, animated: animated)
tableView.isEditing = editing
navigationItem.rightBarButtonItem?.title = editing ? "完成" : "編輯"
}
這個示例稍微復雜绪商,數(shù)據(jù)是從網(wǎng)絡獲得苛谷,首先定義數(shù)據(jù)模型與網(wǎng)絡請求工具:
struct User: CustomStringConvertible {
var firstName: String
var lastName: String
var imageURL: String
var description: String {
return "\(firstName) \(lastName)"
}
}
class UserAPI {
class func getUsers(count: Int) -> Observable<[User]> {
let url = URL(string: "http://api.randomuser.me/?results=\(count)")!
return URLSession.shared.rx.json(url: url).map { (json) -> [User] in
guard let json = json as? [String: AnyObject] else {
fatalError()
}
guard let results = json["results"] as? [[String: AnyObject]] else {
fatalError()
}
return results.map { (info) -> User in
let name = info["name"] as? [String: String]
let picture = info["picture"] as? [String: String]
guard let firstName = name?["first"], let lastName = name?["last"], let imageURL = picture?["large"] else {
fatalError()
}
return User(firstName: firstName, lastName: lastName, imageURL: imageURL)
}
}.share(replay: 1)
}
}
定義枚舉EditingTableViewCommand
表示對UITableView
的操作:
enum EditingTableViewCommand {
case addUsers(users: [User], to: IndexPath)
case moveUser(from: IndexPath, to: IndexPath)
case deleteUser(indexPath: IndexPath)
}
定義EditingTabelViewViewModel
處理UI邏輯:
struct EditingTabelViewViewModel {
static let initalSections: [SectionModel<String, User>] = [
SectionModel<String, User>(model: "Favorite Users", items: [
User(firstName: "Super", lastName: "Man", imageURL: "http://nerdreactor.com/wp-content/uploads/2015/02/Superman1.jpg"),
User(firstName: "Wat", lastName: "Man", imageURL: "http://www.iri.upc.edu/files/project/98/main.GIF")]),
SectionModel<String, User>(model: "Normal Users", items: [User]())
]
private let activity = ActivityIndicator()
let sections: Driver<[SectionModel<String, User>]>
let loading: Driver<Bool>
static func excuteCommand(sections: [SectionModel<String, User>], command: EditingTableViewCommand) -> [SectionModel<String, User>] {
var result = sections
switch command {
case let .addUsers(users, to):
result[to.section].items.insert(contentsOf: users, at: to.row)
case let .moveUser(from, to):
let user = sections[from.section].items[from.row]
result[from.section].items.remove(at: from.row)
result[to.section].items.insert(user, at: to.row)
case let .deleteUser(indexPath):
result[indexPath.section].items.remove(at: indexPath.row)
}
return result
}
init(itemDelete: RxCocoa.ControlEvent<IndexPath>, itemMoved: RxCocoa.ControlEvent<RxCocoa.ItemMovedEvent>) {
self.loading = activity.asDriver(onErrorJustReturn: false)
let add = UserAPI.getUsers(count: 30)
.map { EditingTableViewCommand.addUsers(users: $0, to: IndexPath(row: 0, section: 1)) }
.trackActivity(activity)
sections = Observable.deferred {
let delete = itemDelete.map { EditingTableViewCommand.deleteUser(indexPath: $0) }
let move = itemMoved.map(EditingTableViewCommand.moveUser)
return Observable.merge(add, delete, move)
.scan(EditingTabelViewViewModel.initalSections, accumulator: EditingTabelViewViewModel.excuteCommand(sections:command:))
}.startWith(EditingTabelViewViewModel.initalSections)
.asDriver(onErrorJustReturn: EditingTabelViewViewModel.initalSections)
}
}
- 類型屬性
initalSections
存儲初始數(shù)據(jù),私有屬性activity
用來記錄網(wǎng)絡活動狀態(tài)序列格郁,屬性sections
為UITableView數(shù)據(jù)序列腹殿,屬性loading
為網(wǎng)絡加載狀態(tài)序列 - 類函數(shù)
excuteCommand
根據(jù)對UITableview
的操作EditingTableViewCommand
處理數(shù)據(jù) - 初始化時用私有屬性
activity
轉(zhuǎn)化為Driver
作為loading
屬性 - 初始化時使用
UserAPI
類的getUsers
類型函數(shù)得到一個獲取數(shù)據(jù)的序列,然后使用map
操作符轉(zhuǎn)化為EditingTableViewCommand
操作的序列記為add
- 初始化時將參數(shù)刪除和移動
UITableView
行的序列轉(zhuǎn)化為EditingTableViewCommand
操作的序列分別記為delete
例书、move
- 初始化時將
add
锣尉、delete
、move
這三個序列使用merge
操作符合并為一個序列决采,然后使用scan
操作符掃描序列將類型屬性initalSections
作為初始數(shù)據(jù)自沧、類型函數(shù)excuteCommand
作為轉(zhuǎn)換函數(shù)處理成一個元素為[SectionModel<String, User>]
類型的序列,最后再使用startWith
和asDriver
操作符設置初始元素并轉(zhuǎn)換為Driver
類型的序列
UITableVIew
數(shù)據(jù)復雜綁定實現(xiàn)方式并沒有變化跟簡單綁定是一樣的树瞭,無非就是在對UITableview
進行操作時相應處理需要綁定到UITableview
上的原數(shù)據(jù)如EditingTabelViewViewModel
中的excuteCommand
函數(shù)拇厢,并且在構(gòu)建TableViewSectionedDataSource
時多提供一些canEditRow
,canMoveRow
等閉包為UITableViewDataSource
協(xié)議中定義的函數(shù)提供數(shù)據(jù)支持晒喷。
在控制器中構(gòu)建一個懶加載屬性dataSource
提供Cell
生成孝偎、組頭部標題、是否可以編輯凉敲、是否可以移動等閉包:
lazy var dataSource: TableViewSectionedDataSource<SectionModel<String, User>> = { TableViewSectionedDataSource<SectionModel<String, User>>(cellForRow: { ds, tv, indexPath in
let cell = CommonCell.cellFor(tableView: tv)
cell.accessoryType = UITableViewCell.AccessoryType.disclosureIndicator
cell.textLabel?.text = ds[indexPath].firstName + " " + ds[indexPath].lastName
return cell
}, titleForHeader: { "\($0[$2].model)>\($0[$2].items)" }, canEditRow: { _,_,_ in true }, canMoveRow: { _,_,_ in true }) }()
擴展Reactive
用來綁定加載動畫:
extension Reactive where Base: UIViewController & NVActivityIndicatorViewable {
var animating: Binder<Bool> {
return Binder(base) { (t, v) in
if v != t.isAnimating {
if v {
t.startAnimating()
} else {
t.stopAnimating()
}
}
UIApplication.shared.isNetworkActivityIndicatorVisible = v
}
}
}
最后構(gòu)建EditingTabelViewViewModel
衣盾,進行數(shù)據(jù)綁定:
let viewModel: EditingTabelViewViewModel = EditingTabelViewViewModel(itemDelete: tableView.rx.itemDeleted, itemMoved: tableView.rx.itemMoved)
viewModel.loading
.drive(self.rx.animating)
.disposed(by: bag)
viewModel.sections
.drive(tableView.rx.items(dataSource: dataSource))
.disposed(by: bag)
tableView.rx
.modelSelected(User.self)
.subscribe(onNext: { [weak self] (user) in
let viewController = UIStoryboard(name: "EditingTableView", bundle: Bundle.main).instantiateViewController(withIdentifier: "DetailViewController") as! DetailViewController
viewController.user = user
self?.navigationController?.pushViewController(viewController, animated: true)
}).disposed(by: bag)
tableView.rx
.itemSelected
.subscribe(onNext: { [weak self] in self!.tableView.deselectRow(at: $0, animated: true) })
.disposed(by: bag)
擴展
參考UIPickerView的Rx實現(xiàn),其實還可以定義TableViewSectionedDataSource
的子類爷抓,讓其遵守UITableviewDelegate
協(xié)議势决,進而可以實現(xiàn)對UITableview
的行高、組頭部高等進行綁定蓝撇。