RxSwift-MVVM
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) incell.setUIData(model as! HBModel) }.disposed(by: disposeBag)复制代码
初始化一个 BehaviorSuject
序列,并加载 cell
。到这里我们就可以展示一个列表了,至于 cell
样式我们就常规创建设置。到此仅仅两步我们就能看到一个完整列表,很简洁,很高效。
这里很像我们之前在 OC
里边拆分代理实现一样, RxSwift
帮我们实现了内部方法。
3、实现点击事件
tableview.rx.itemSelected.subscribe(onNext: {[weak self] (indexPath) inprint("点击\(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) inprint("删除\(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) inprint("添加数据:\(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 = truetableview.rx.itemMoved.subscribe(onNext: {[weak self] (sourceIndex, destinationIndex) inprint("从\(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: Stringlet 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{ 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 inlet 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 inreturn dataSource.sectionModels[index].header } //展示尾部视图 dataSource.titleForFooterInSection = {(dataSource,index) -> String inreturn "\(dataSource.sectionModels[index].header) 尾部视图"} //设置可编辑-根据不同组来设置是否可编辑 dataSource.canEditRowAtIndexPath = {data,indexPath inreturn true} //设置可移动-根据不同组来设置是否可移动 dataSource.canMoveRowAtIndexPath = {data,indexPath inreturn true}复制代码
效果如下:

四、MVVM双向绑定
有个搜索列表需求,搜索框输入文本,发出请求,在将数据加载到 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 = {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{ 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 inlet cell = tableview.dequeueReusableCell(withIdentifier: self!.resuseID) as! RowViewCell2 cell.selectionStyle = .none cell.nameLabel.text = model.name cell.detailLabel.text = model.urlreturn cell }.disposed(by: disposeBag)复制代码
-
通过
drive
发送请求到的共享数据,将数据绑定到tableview
上显示
最终实现效果如下:

通过以上的对 RxSwift
的使用体验,我们会发现,在 RxSwift
中省略了所有事件的创建,点击事件,编辑事件,按钮事件等等,在哪创建 UI
,就在哪使用,事件的产生由 RxSwift
直接提供, UI
的展示也可以直接交给 RxSwift
来赋值。我们需要做的是:数据和 UI
的相互绑定。
在没有接触 RAC
和 RxSwift
之前,个人也是封装了这些事件,便于调用,但是数据绑定上并没考虑到太多,道行尚浅还需继续学习。