一、基本使用
① 单分区的表格
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
var tableView:UITableView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let items = Observable.just([
"文本输入框的用法",
"开关按钮的用法",
"进度条的用法",
"文本标签的用法",
])
items
.bind(to: tableView.rx.items) {
(tableView, row, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(row):\(element)"
return cell
}
.disposed(by: disposeBag)
}
}
② 单元格选中事件响应
- 当点击某个单元格时将其索引位置,以及对应的标题打印出来:
选中项的indexPath为:[0, 3]
选中项的标题为:文本标签的用法
tableView.rx.itemSelected.subscribe(onNext: {
indexPath in
print("选中项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)
tableView.rx.modelSelected(String.self).subscribe(onNext: {
item in
print("选中项的标题为:\(item)")
}).disposed(by: disposeBag)
tableView.rx.itemSelected.subscribe(onNext: {
[weak self] indexPath in
print("选中项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)
tableView.rx.modelSelected(String.self).subscribe(onNext: {
[weak self] item in
print("选中项的标题为:\(item)")
}).disposed(by: disposeBag)
③ 单元格取消选中事件响应
被取消选中项的indexPath为:[0, 2]
被取消选中项的的标题为:进度条的用法
tableView.rx.itemDeselected.subscribe(onNext: {
[weak self] indexPath in
print("被取消选中项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)
tableView.rx.modelDeselected(String.self).subscribe(onNext: {
[weak self] item in
print("被取消选中项的的标题为:\(item)")
}).disposed(by: disposeBag)
④ 单元格删除事件响应
- 如下所示,左滑删除:
- 示例代码:
tableView.rx.itemDeleted.subscribe(onNext: {
indexPath in
print("删除项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)
tableView.rx.modelDeleted(String.self).subscribe(onNext: {
item in
print("删除项的的标题为:\(item)")
}).disposed(by: disposeBag)
⑤ 单元格移动事件响应
移动项原来的indexPath为:[0, 0]
移动项现在的indexPath为:[0, 1]
tableView.rx.itemMoved.subscribe(onNext: {
sourceIndexPath, destinationIndexPath in
print("移动项原来的indexPath为:\(sourceIndexPath)")
print("移动项现在的indexPath为:\(destinationIndexPath)")
}).disposed(by: disposeBag)
⑥ 单元格插入事件响应
插入项的indexPath为:[0, 1]
tableView.rx.itemInserted.subscribe(onNext: {
indexPath in
print("插入项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)
⑦ 单元格尾部附件(图标)点击事件响应
尾部项的indexPath为:[0, 1]
tableView.rx.itemAccessoryButtonTapped.subscribe(onNext: {
indexPath in
print("尾部项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)
⑧ 单元格将要显示出来的事件响应
将要显示单元格indexPath为:[0, 0]
将要显示单元格cell为:<UITableViewCell: 0x7fe74e925fd0; frame = (0 0; 428 45); text = '0:文本输入框的用法'; autoresize = W; layer = <CALayer: 0x600003d77560>>
将要显示单元格indexPath为:[0, 1]
将要显示单元格cell为:<UITableViewCell: 0x7fe74eb16080; frame = (0 45; 428 45); text = '1:开关按钮的用法'; autoresize = W; layer = <CALayer: 0x600003d12de0>>
将要显示单元格indexPath为:[0, 2]
将要显示单元格cell为:<UITableViewCell: 0x7fe74ea0b1b0; frame = (0 90; 428 45); text = '2:进度条的用法'; autoresize = W; layer = <CALayer: 0x600003d1a700>>
将要显示单元格indexPath为:[0, 3]
将要显示单元格cell为:<UITableViewCell: 0x7fe74ea0c670; frame = (0 135; 428 45); text = '3:文本标签的用法'; autoresize = W; layer = <CALayer: 0x600003d1a9e0>>
tableView.rx.willDisplayCell.subscribe(onNext: {
cell, indexPath in
print("将要显示单元格indexPath为:\(indexPath)")
print("将要显示单元格cell为:\(cell)\n")
}).disposed(by: disposeBag)
二、RxDataSources
- 如果 tableview 需要显示多个 section 或者更加复杂的编辑功能时,可以借助 RxDataSource 这个第三方库来帮我们完成。
- RxDataSource 的本质就是使用 RxSwift 对 UITableView 和 UICollectionView 的数据源做了一层包装,使用它可以大大减少我们的工作量。
① 安装配置
pod 'RxDataSources', '~> 3.0'
github "RxSwiftCommunity/RxDataSources" ~> 3.0
- 手动安装:
-
- 在 RxDataSources 上将 RxDataSources 下载到本地,并引入到项目中来:
import RxDataSources
② 单分区的 TableView
- RxDataSources 是以 section 来做为数据结构的,所以不管 tableView 是单分区还是多分区,在使用 RxDataSources 的过程中,都需要返回一个 section 的数组。
- 使用自带的 Section:
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let items = Observable.just([
SectionModel(model: "", items: [
"UILable的用法",
"UIText的用法",
"UIButton的用法"
])
])
let dataSource = RxTableViewSectionedReloadDataSource
<SectionModel<String, String>>(configureCell: {
(dataSource, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(indexPath.row):\(element)"
return cell
})
items
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: DisposeBag())
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let sections = Observable.just([
MySection(header: "", items: [
"UILable的用法",
"UIText的用法",
"UIButton的用法"
])
])
let dataSource = RxTableViewSectionedAnimatedDataSource<MySection>(
configureCell: {
ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")
?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.textLabel?.text = "\(ip.row):\(item)"
return cell
})
sections
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: DisposeBag())
struct MySection {
var header: String
var items: [Item]
}
extension MySection : AnimatableSectionModelType {
typealias Item = String
var identity: String {
return header
}
init(original: MySection, items: [Item]) {
self = original
self.items = items
}
}
③ 多分区的 UITableView
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let items = Observable.just([
SectionModel(model: "基本控件", items: [
"UILable的用法",
"UIText的用法",
"UIButton的用法"
]),
SectionModel(model: "高级控件", items: [
"UITableView的用法",
"UICollectionViews的用法"
])
])
let dataSource = RxTableViewSectionedReloadDataSource
<SectionModel<String, String>>(configureCell: {
(dataSource, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "\(indexPath.row):\(element)"
return cell
})
dataSource.titleForHeaderInSection = {
ds, index in
return ds.sectionModels[index].model
}
items
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let sections = Observable.just([
MySection(header: "基本控件", items: [
"UILable的用法",
"UIText的用法",
"UIButton的用法"
]),
MySection(header: "高级控件", items: [
"UITableView的用法",
"UICollectionViews的用法"
])
])
let dataSource = RxTableViewSectionedAnimatedDataSource<MySection>(
configureCell: {
ds, tv, ip, item in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")
?? UITableViewCell(style: .default, reuseIdentifier: "Cell")
cell.textLabel?.text = "\(ip.row):\(item)"
return cell
},
titleForHeaderInSection: {
ds, index in
return ds.sectionModels[index].header
}
struct MySection {
var header: String
var items: [Item]
}
extension MySection : AnimatableSectionModelType {
typealias Item = String
var identity: String {
return header
}
init(original: MySection, items: [Item]) {
self = original
self.items = items
}
}
三、刷新表格数据
- 很多情况下,表格里的数据不是一开始就准备好的、或者固定不变的,可能需要先向服务器请求数据,再将获取到的内容显示在表格中。
- 要重新加载表格数据,过去的做法就是调用 tableView 的 reloadData() 方法,那么在使用 RxSwift 的情况下,应该如何刷新表格的数据呢?
① 数据刷新
- 如下所示:
-
- 界面初始化完毕后,tableView 默认会加载一些随机数据;
-
- 点击右上角的刷新按钮,tableView 会重新加载并显示一批新数据;
-
- 为方便演示,每次获取数据不是真的去发起网络请求,而是在本地生成后延迟 2 秒返回,模拟这种异步请求的情况。
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self,
forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let randomResult = refreshButton.rx.tap.asObservable()
.startWith(())
.flatMapLatest(getRandomResult)
.share(replay: 1)
let dataSource = RxTableViewSectionedReloadDataSource
<SectionModel<String, Int>>(configureCell: {
(dataSource, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "条目\(indexPath.row):\(element)"
return cell
})
randomResult
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
func getRandomResult() -> Observable<[SectionModel<String, Int>]> {
print("正在请求数据...")
let items = (0 ..< 5).map {
_ in
Int(arc4random())
}
let observable = Observable.just([SectionModel(model: "S", items: items)])
return observable.delay(2, scheduler: MainScheduler.instance)
}
② 防止表格多次刷新
- flatMapLatest 的作用是当在短时间内(上一个请求还没回来)连续点击多次“刷新”按钮,虽然仍会发起多次请求,但表格只会接收并显示最后一次请求,避免表格出现连续刷新的现象:
let randomResult = refreshButton.rx.tap.asObservable()
.startWith(())
.flatMapLatest(getRandomResult)
.share(replay: 1)
- 也可以对源头进行限制,即通过 throttle 设置个阀值(比如 1 秒),如果在1秒内有多次点击则只取最后一次,那么自然也就只发送一次数据请求:
let randomResult = refreshButton.rx.tap.asObservable()
.throttle(1, scheduler: MainScheduler.instance)
.startWith(())
.flatMapLatest(getRandomResult)
.share(replay: 1)
③ 停止数据请求
- 在实际项目中我们可能会需要对一个未完成的网络请求进行中断操作,比如切换页面或者分类时,如果上一次的请求还未完成就要将其取消掉。那么 RxSwift 如何实现该功能呢?
- 该功能简单说就是通过 takeUntil 操作符实现,当 takeUntil 中的 Observable 发送一个值时,便会结束对应的 Observable:
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self,
forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let randomResult = refreshButton.rx.tap.asObservable()
.startWith(())
.flatMapLatest{
self.getRandomResult().takeUntil(self.cancelButton.rx.tap)
}
.share(replay: 1)
let dataSource = RxTableViewSectionedReloadDataSource
<SectionModel<String, Int>>(configureCell: {
(dataSource, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "条目\(indexPath.row):\(element)"
return cell
})
randomResult
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
func getRandomResult() -> Observable<[SectionModel<String, Int>]> {
print("正在请求数据......")
let items = (0 ..< 5).map {
_ in
Int(arc4random())
}
let observable = Observable.just([SectionModel(model: "S", items: items)])
return observable.delay(2, scheduler: MainScheduler.instance)
}
四、表格数据的搜索过滤
- 在 tableView 的表头上增加了一个搜索框,tableView 会根据搜索框里输入的内容实时地筛选并显示出符合条件的数据(包含有输入文字的数据条目)。
- 这个实时搜索是对已获取到的数据进行过滤,即每次输入文字时不会重新发起请求。
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self,
forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
self.searchBar = UISearchBar(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: 56))
self.tableView.tableHeaderView = self.searchBar
let randomResult = refreshButton.rx.tap.asObservable()
.startWith(())
.flatMapLatest(getRandomResult)
.flatMap(filterResult)
.share(replay: 1)
let dataSource = RxTableViewSectionedReloadDataSource
<SectionModel<String, Int>>(configureCell: {
(dataSource, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "条目\(indexPath.row):\(element)"
return cell
})
randomResult
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
func getRandomResult() -> Observable<[SectionModel<String, Int>]> {
print("正在请求数据...")
let items = (0 ..< 5).map {
_ in
Int(arc4random())
}
let observable = Observable.just([SectionModel(model: "S", items: items)])
return observable.delay(2, scheduler: MainScheduler.instance)
}
func filterResult(data:[SectionModel<String, Int>]) -> Observable<[SectionModel<String, Int>]> {
return self.searchBar.rx.text.orEmpty
.flatMapLatest{
query -> Observable<[SectionModel<String, Int>]> in
print("正在筛选数据(条件为:\(query))")
if query.isEmpty{
return Observable.just(data)
}
else{
var newData:[SectionModel<String, Int>] = []
for sectionModel in data {
let items = sectionModel.items.filter{
"\($0)".contains(query) }
newData.append(SectionModel(model: sectionModel.model, items: items))
}
return Observable.just(newData)
}
}
}
五、可编辑表格
① 实现效果
- 程序启动后表格会自动加载 5 条随机数据,点击“刷新”按钮则又重新生成 5 条数据并显示;
- 点击“加号”图标后,会在当前数据集的末尾添加一条随机数据并显示;
- 点击单元格左侧的“减号”图标则可以将该行数据删除;
- 拖动单元格右侧的控制图标可以改变显示顺序。
② 示例代码
- 由于编辑操作比较多,使用 enum 创建一个命令枚举,里面定义了对 tableView 数据的各种操作:
enum TableEditingCommand {
case setItems(items: [String])
case addItem(item: String)
case moveItem(from: IndexPath, to: IndexPath)
case deleteItem(IndexPath)
}
- 接着定义 tableView 对应的 ViewModel,这里面除了保存有表格数据外,还包含上面定义的 4 个操作命令的具体实现:
struct TableViewModel {
fileprivate var items:[String]
init(items: [String] = []) {
self.items = items
}
func execute(command: TableEditingCommand) -> TableViewModel {
switch command {
case .setItems(let items):
print("设置表格数据。")
return TableViewModel(items: items)
case .addItem(let item):
print("新增数据项。")
var items = self.items
items.append(item)
return TableViewModel(items: items)
case .moveItem(let from, let to):
print("移动数据项。")
var items = self.items
items.insert(items.remove(at: from.row), at: to.row)
return TableViewModel(items: items)
case .deleteItem(let indexPath):
print("删除数据项。")
var items = self.items
items.remove(at: indexPath.row)
return TableViewModel(items: items)
}
}
}
class ViewController: UIViewController {
@IBOutlet weak var refreshButton: UIBarButtonItem!
@IBOutlet weak var addButton: UIBarButtonItem!
var tableView:UITableView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView = UITableView(frame: self.view.frame, style:.plain)
self.tableView!.register(UITableViewCell.self,
forCellReuseIdentifier: "Cell")
self.view.addSubview(self.tableView!)
let initialVM = TableViewModel()
let refreshCommand = refreshButton.rx.tap.asObservable()
.startWith(())
.flatMapLatest(getRandomResult)
.map(TableEditingCommand.setItems)
let addCommand = addButton.rx.tap.asObservable()
.map{
"\(arc4random())" }
.map(TableEditingCommand.addItem)
let movedCommand = tableView.rx.itemMoved
.map(TableEditingCommand.moveItem)
let deleteCommand = tableView.rx.itemDeleted.asObservable()
.map(TableEditingCommand.deleteItem)
Observable.of(refreshCommand, addCommand, movedCommand, deleteCommand)
.merge()
.scan(initialVM) {
(vm: TableViewModel, command: TableEditingCommand)
-> TableViewModel in
return vm.execute(command: command)
}
.startWith(initialVM)
.map {
[AnimatableSectionModel(model: "", items: $0.items)]
}
.share(replay: 1)
.bind(to: tableView.rx.items(dataSource: ViewController.dataSource()))
.disposed(by: disposeBag)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
tableView.setEditing(true, animated: true)
}
func getRandomResult() -> Observable<[String]> {
print("生成随机数据。")
let items = (0 ..< 5).map {
_ in
"\(arc4random())"
}
return Observable.just(items)
}
}
extension ViewController {
static func dataSource() -> RxTableViewSectionedAnimatedDataSource
<AnimatableSectionModel<String, String>> {
return RxTableViewSectionedAnimatedDataSource(
animationConfiguration: AnimationConfiguration(insertAnimation: .top,
reloadAnimation: .fade,
deleteAnimation: .left),
configureCell: {
(dataSource, tv, indexPath, element) in
let cell = tv.dequeueReusableCell(withIdentifier: "Cell")!
cell.textLabel?.text = "条目\(indexPath.row):\(element)"
return cell
},
canEditRowAtIndexPath: {
_, _ in
return true
},
canMoveRowAtIndexPath: {
_, _ in
return true
}
)
}
}
六、不同类型的单元格混用
① 效果展示
- tableView 绑定的数据源中一共有 2 个 section,每个 section 里分别有 3 条数据需要显示。
- 每个 cell 会根据数据类型的不同,自动选择相应的显示方式:“文字+图片”或“文字+开关按钮”。
② 示例代码
let sections = Observable.just([
MySection(header: "我是第一个分区", items: [
.TitleImageSectionItem(title: "图片数据1", image: UIImage(named: "php")!),
.TitleImageSectionItem(title: "图片数据2", image: UIImage(named: "react")!),
.TitleSwitchSectionItem(title: "开关数据1", enabled: true)
]),
MySection(header: "我是第二个分区", items: [
.TitleSwitchSectionItem(title: "开关数据2", enabled: false),
.TitleSwitchSectionItem(title: "开关数据3", enabled: false),
.TitleImageSectionItem(title: "图片数据3", image: UIImage(named: "swift")!)
])
])
let dataSource = RxTableViewSectionedReloadDataSource<MySection>(
configureCell: {
dataSource, tableView, indexPath, item in
switch dataSource[indexPath] {
case let .TitleImageSectionItem(title, image):
let cell = tableView.dequeueReusableCell(withIdentifier: "titleImageCell",
for: indexPath)
(cell.viewWithTag(1) as! UILabel).text = title
(cell.viewWithTag(2) as! UIImageView).image = image
return cell
case let .TitleSwitchSectionItem(title, enabled):
let cell = tableView.dequeueReusableCell(withIdentifier: "titleSwitchCell",
for: indexPath)
(cell.viewWithTag(1) as! UILabel).text = title
(cell.viewWithTag(2) as! UISwitch).isOn = enabled
return cell
}
},
titleForHeaderInSection: {
ds, index in
return ds.sectionModels[index].header
}
)
sections
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
enum SectionItem {
case TitleImageSectionItem(title: String, image: UIImage)
case TitleSwitchSectionItem(title: String, enabled: Bool)
}
struct MySection {
var header: String
var items: [SectionItem]
}
extension MySection : SectionModelType {
typealias Item = SectionItem
init(original: MySection, items: [Item]) {
self = original
self.items = items
}
}