RxSwift-MVVM

MVVM 核心在于数据与 UI 的双向绑定,数据的变化会更新 UIUI 变化会更新我们的数据。那这种绑定操作谁来做呢?当然是我们的 RxSwift 。学习 RxSwift 框架以来,似乎并没有真正使用过这个框架,下面就来看看, RxSwift 具体能带来哪些便利。

一、登录页面

先看看效果:

UI 页面代码省略,下面只看数据 UI 是如何绑定的。

1、 UISwitchUILabel 的绑定

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)复制代码

rxisOn 属性值绑定到 labelisHidden 属性上, UI 改变 isOn 属性同时给 label 的属性赋值,两个属性类型同为 Bool 类型。

2、 UITextFieldUILabel 的绑定

nameTf.rx.text.bind(to: inputLabel.rx.text).disposed(by: disposeBag)
paswdTf.rx.text.bind(to: inputLabel.rx.text).disposed(by: disposeBag)复制代码

输入值 text 改变,同时改变 inputLabeltext 属性。

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隐藏提示文本,以上序列满足条件发送的是 trueisHidden=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 精简了,这里还是需要我们一步步创建实现。当然这里我们可以看到我们并没有遵循 delegatedataSource 代理。

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 结构体,定义 ItemCustomData 类型, IdentityString 类型

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 的相互绑定。

在没有接触 RACRxSwift 之前,个人也是封装了这些事件,便于调用,但是数据绑定上并没考虑到太多,道行尚浅还需继续学习。