MVVM
核心在于数据与UI
的双向绑定,数据的变化会更新UI
,UI
变化会更新我们的数据。那这种绑定操作谁来做呢?当然是我们的RxSwift
。学习RxSwift
框架以来,似乎并没有真正使用过这个框架,下面就来看看,RxSwift
具体能带来哪些便利。
一、登录页面
先看看效果:
UI
页面代码省略,下面只看数据UI
是如何绑定的。
1、UISwitch
和UILabel
的绑定
switch1.rx.isOn.map{!$0}.bind(to: titleLabel.rx.isHidden).disposed(by: disposeBag)
switch1.rx.isOn.map{!$0}.bind(to: inputLabel.rx.isHidden).disposed(by: disposeBag)
rx
将isOn
属性值绑定到label
的isHidden
属性上,UI
改变isOn
属性同时给label
的属性赋值,两个属性类型同为Bool
类型。
2、UITextField
和UILabel
的绑定
nameTf.rx.text.bind(to: inputLabel.rx.text).disposed(by: disposeBag)
paswdTf.rx.text.bind(to: inputLabel.rx.text).disposed(by: disposeBag)
输入值text
改变,同时改变inputLabel
的text
属性。
3、绑定提示文本
let nameVerify = nameTf.rx.text.orEmpty.map{$0.count>5}
nameVerify.bind(to: nameLabel.rx.isHidden).disposed(by: disposeBag)
let pawdVerify = paswdTf.rx.text.orEmpty.map{$0.count>5}
pawdVerify.bind(to: paswdLabel.rx.isHidden).disposed(by: disposeBag)
通常一些提示语需要跟随输入来改变,如上通过map
设置条件,将序列绑定到相应的UI
控件上,控制显隐。当输入文本字符大于5隐藏提示文本,以上序列满足条件发送的是true
,isHidden=true
即为隐藏。
4、联合绑定
Observable.combineLatest(nameVerify,pawdVerify){
$0 && $1
}.bind(to: loginBtn.rx.isEnabled).disposed(by: disposeBag)
结合两个用户名和密码两个条件来控制登录按钮是否可以点击。combineLatest
合并为新序列,两个条件同时成立即使能登录按钮。
通过以上的演示,明显能够感受到
RxSwift
给我们带来的便捷。通常需要我们设置触发事件,在触发事件中来赋值展示,代码过长,业务与UI
分散不好管理,在RxSwift
中只需要一两行代码便可以完成事件的创建与监听以及赋值。
二、UITableView列表展示
先看一下RxSwift
实现的效果:
展示上没有特别之处。在常规写法中,需要遵循代理并实现代理方法,在RxSwift
中我们可以如下写法:
1、创建tableView
tableview = UITableView.init(frame: self.view.bounds,style: .plain)
tableview.tableFooterView = UIView()
tableview.register(RowViewCell.classForCoder(), forCellReuseIdentifier: resuseID)
tableview.rowHeight = 100
self.view.addSubview(tableview)
常规写法,RxSwift
再精简也不能把我们的UI
精简了,这里还是需要我们一步步创建实现。当然这里我们可以看到我们并没有遵循delegate
和dataSource
代理。
2、初始化序列并展示
let dataOB = BehaviorSubject.init(value: self.viewModel.dataArray)
dataOB.asObserver().bind(to: tableview.rx.items(cellIdentifier:resuseID, cellType: RowViewCell.self)){(row, model, cell) in
cell.setUIData(model as! HBModel)
}.disposed(by: disposeBag)
初始化一个BehaviorSuject
序列,并加载cell
。到这里我们就可以展示一个列表了,至于cell
样式我们就常规创建设置。到此仅仅两步我们就能看到一个完整列表,很简洁,很高效。
这里很像我们之前在
OC
里边拆分代理实现一样,RxSwift
帮我们实现了内部方法。
3、实现点击事件
tableview.rx.itemSelected.subscribe(onNext: {[weak self] (indexPath) in
print("点击\(indexPath)行")
self?.navigationController!.pushViewController(SectionTableview.init(), animated: true)
self?.tableview.deselectRow(at: indexPath, animated: true)
}).disposed(by: disposeBag)
这里把所有点击事件当做序列来处理像观察者发送点击消息。
4、删除一个cell
tableview.delegate = self
tableview.rx.itemDeleted.subscribe(onNext: {[weak self] (indexPath) in
print("删除\(indexPath)行")
self!.viewModel.dataArray.remove(at: indexPath.row)
self?.loadUI(obSubject: dataOB)
}).disposed(by: disposeBag)
extension RowTableview: UITableViewDelegate{
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
}
这里需要我们遵循代理,并实现以上方法,设置删除类型。
5、新增一个cell
tableview.delegate = self
tableview.rx.itemInserted.subscribe(onNext: {[weak self] (indexPath) in
print("添加数据:\(indexPath)行")
guard let model = self?.viewModel.dataArray.last else{
print("数据相等不太好添加")
return
}
self?.viewModel.dataArray.insert(model, at: indexPath.row)
self?.loadUI(obSubject: dataOB)
}).disposed(by: disposeBag)
extension RowTableview: UITableViewDelegate{
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .insert
}
}
同上遵循代理,实现方法,设置为插入类型。
6、移动cell
位置
tableview.isEditing = true
tableview.rx.itemMoved.subscribe(onNext: {[weak self] (sourceIndex, destinationIndex) in
print("从\(sourceIndex)移动到\(destinationIndex)")
self?.viewModel.dataArray.swapAt(sourceIndex.row, destinationIndex.row)
self?.loadUI(obSubject: dataOB)
}).disposed(by: disposeBag)
设置为可编辑既可以出现删除图标去,和移动图标。
- 使用
tableview
响应的功能,只需通过tableview
调用相应的序列,并订阅即可- 移动、新增
cell
需要我们实现UITableViewDelegate
代理方法,设置相应的EditingStyle- 同
cell
不同行高,也需要我们实现UITableViewDelegate
的代理方法,根据不同类型返回不同行高
三、UITableView的组实现
1、先创建tableview
视图
//列表
tableview = UITableView.init(frame: self.view.bounds,style: .plain)
tableview.tableFooterView = UIView()
tableview.register(RowViewCell1.classForCoder(), forCellReuseIdentifier: resuseID)
tableview.rowHeight = 80
tableview.delegate = self//此处遵循协议-实现编辑类型 删除、增加,设置头尾视图高度
self.view.addSubview(tableview)
- 设置
delegate
可以实现cell
的编辑类型(删除、增加)设置头尾视图高度
2、创建一个Model
文件,声明一个结构体设置我们需要显示的属性
struct CustomData {
let name: String
let gitHubID: String
var image: UIImage?
init(name:String, gitHubID:String) {
self.name = name
self.gitHubID = gitHubID
image = UIImage(named: gitHubID)
}
}
- 每一条展示的数据都是从结构体中获取
3、创建组信息结构体
struct SectionOfCustomData {
var header: Identity
var items: [Item]
}
extension SectionOfCustomData: SectionModelType{
typealias Item = CustomData
typealias Identity = String
var identity: Identity{
return header
}
init(original: SectionOfCustomData, items: [Item]) {
self = original
self.items = items
}
}
-
header
头部标题字符串 -
items
数组结构,用来存放步骤1中的结构体对象 - 扩展
SectionOfCustomData
结构体,定义Item
为CustomData
类型,Identity
为String
类型
4、创建一个数据源类,并设置数据
class CustomDataList {
var dataArrayOb:Observable<[SectionOfCustomData]>{
get{
return Observable.just(dataArray)
}
}
var dataArray = [
SectionOfCustomData(header: "A", items: [
CustomData(name: "Alex V Bush", gitHubID: "alexvbush"),
CustomData(name: "Andrew Breckenridge", gitHubID: "AndrewSB"),
CustomData(name: "Anton Efimenko", gitHubID: "reloni"),
CustomData(name: "Ash Furrow", gitHubID: "ashfurrow")
]),
SectionOfCustomData(header: "B", items: [
CustomData(name: "Alex V Bush", gitHubID: "alexvbush"),
CustomData(name: "Andrew Breckenridge", gitHubID: "AndrewSB"),
CustomData(name: "Anton Efimenko", gitHubID: "reloni"),
CustomData(name: "Ash Furrow", gitHubID: "ashfurrow")
]),
SectionOfCustomData(header: "C", items: [
CustomData(name: "Alex V Bush", gitHubID: "alexvbush"),
CustomData(name: "Andrew Breckenridge", gitHubID: "AndrewSB"),
CustomData(name: "Anton Efimenko", gitHubID: "reloni"),
CustomData(name: "Ash Furrow", gitHubID: "ashfurrow")
]),
]
}
- 创建数组,存放定义的数据结构,并设置每组信息
- 将数组插入到可观察序列中,用来想绑定对象发送元素
5、创建数据源对象,数据类型为SectionOfCustomData
let dataSource = RxTableViewSectionedReloadDataSource(configureCell: {[weak self] (dataSource, tableView, indexPath, HBSectionModel) -> RowViewCell1 in
let cell = tableView.dequeueReusableCell(withIdentifier: self!.resuseID, for: indexPath) as! RowViewCell1
cell.selectionStyle = .none
cell.setSectionUIData(dataSource.sectionModels[indexPath.section].items[indexPath.row])
return cell
})
点击查看该类,进入内部查看,该类继承了TableViewSectionedDataSource
类,在改类中,实际上实现了外部tableview
的所有UITableViewDataSource
的代理方法,通过闭包属性,将代理方法中的处理交给外部实现。
public typealias ConfigureCell = (TableViewSectionedDataSource, UITableView, IndexPath, Item) -> UITableViewCell
public typealias TitleForHeaderInSection = (TableViewSectionedDataSource, Int) -> String?
public typealias TitleForFooterInSection = (TableViewSectionedDataSource, Int) -> String?
public typealias CanEditRowAtIndexPath = (TableViewSectionedDataSource, IndexPath) -> Bool
public typealias CanMoveRowAtIndexPath = (TableViewSectionedDataSource, IndexPath) -> Bool
外部实现如下:
//展示头视图
dataSource.titleForHeaderInSection = {(dataSource,index) -> String in
return dataSource.sectionModels[index].header
}
//展示尾部视图
dataSource.titleForFooterInSection = {(dataSource,index) -> String in
return "\(dataSource.sectionModels[index].header) 尾部视图"
}
//设置可编辑-根据不同组来设置是否可编辑
dataSource.canEditRowAtIndexPath = {data,indexPath in
return true
}
//设置可移动-根据不同组来设置是否可移动
dataSource.canMoveRowAtIndexPath = {data,indexPath in
return true
}
效果如下:
四、search搜索请求实现
有个搜索列表需求,搜索框输入文本,发出请求,在将数据加载到tableview
列表中。UI
常规操作,不做描述。通常我们需要添加输入事件,在事件方法中发送网络请求,再将数据加载到tableview
上。而在的RxSwift
中呢,我们不需复杂的操作,只需要将UI
绑定到序列上,序列在绑定至UI
上即可。
1、创建数据Model
类
class searchModel: HandyJSON {
var name: String = ""
var url: String = ""
required init() {
}
init(name:String,url:String) {
self.name = name
self.url = url
}
}
- 存放用来展示的属性,提供初始化方法
- 继承自
HandyJSON
,能够帮助我们序列化请求过来的数据
2、创建viewModel
类
class SearchViewModel: NSObject {
//1、创建一个序列
let searchOB = BehaviorSubject(value: "")
lazy var searchData: Driver<[searchModel]> = {
return self.searchOB.asObservable()
.throttle(RxTimeInterval.milliseconds(300), scheduler: MainScheduler.instance)//设置300毫秒发送一次消息
.distinctUntilChanged()//搜索框内容改变才发送消息
.flatMapLatest(SearchViewModel.responseData)
.asDriver(onErrorJustReturn: [])
}()
//2、请求数据
static func responseData(_ githubID:String) -> Observable<[searchModel]>{
guard !githubID.isEmpty, let url = URL(string: "https://api.github.com/users/\(githubID)/repos")else{
return Observable.just([])
}
return URLSession.shared.rx.json(url: url)
.retry()//请求失败尝试重新请求一次
.observeOn(ConcurrentDispatchQueueScheduler(qos: .background))//后台下载
.map(SearchViewModel.dataParse)
}
//3、数据序列化
static func dataParse(_ json:Any) -> [searchModel]{
//字典+数组
guard let items = json as? [[String:Any]] else {return []}
//序列化
guard let result = [searchModel].deserialize(from: items) else {return []}
return result as! [searchModel]
}
}
- 创建一个
BehaviorSubject
类型的序列,可做序列生产者又可做观察者 -
searchData
输入的入口,触发搜索获取网络数据 -
throttle
设定消息发送时间间隔,避免频繁请求 -
distinctUntilChanged
只有输入内容发生变化才发出消息 -
flatMapLatest
序列的序列需要下沉请求,回调结果 -
asDriver
使得序列为Driver
序列,保证状态共享,不重复发送请求,保证消息发送在主线程
3、双向绑定
搜索框绑定到序列:
self.searchBar.rx.text.orEmpty
.bind(to: self.viewModel.searchOB).disposed(by: disposeBag)
- 绑定序列,输入时会向序列发送消息,开始请求数据并保存
绑定UI->tableview
:
self.viewModel.searchData.drive(self.tableview.rx.items) {[weak self] (tableview,indexPath,model) -> RowViewCell2 in
let cell = tableview.dequeueReusableCell(withIdentifier: self!.resuseID) as! RowViewCell2
cell.selectionStyle = .none
cell.nameLabel.text = model.name
cell.detailLabel.text = model.url
return cell
}.disposed(by: disposeBag)
- 通过
drive
发送请求到的共享数据,将数据绑定到tableview
上显示
最终实现效果如下:
通过以上的对
RxSwift
的使用体验,我们会发现,在RxSwift
中省略了所有事件的创建,点击事件,编辑事件,按钮事件等等,在哪创建UI
,就在哪使用,事件的产生由RxSwift
直接提供,UI
的展示也可以直接交给RxSwift
来赋值。我们需要做的是:数据和UI
的相互绑定。
在没有接触
RAC
和RxSwift
之前,个人也是封装了这些事件,便于调用,但是数据绑定上并没考虑到太多,道行尚浅还需继续学习。