swiftui DateComponentsFormatter 本地化 swiftui mvvm_git

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

一、登录页面

先看看效果:

swiftui DateComponentsFormatter 本地化 swiftui mvvm_sed_02

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实现的效果:

swiftui DateComponentsFormatter 本地化 swiftui mvvm_git_03

展示上没有特别之处。在常规写法中,需要遵循代理并实现代理方法,在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) 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结构体,定义ItemCustomData类型,IdentityString类型

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<SectionOfCustomData>(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<Section>, UITableView, IndexPath, Item) -> UITableViewCell
public typealias TitleForHeaderInSection = (TableViewSectionedDataSource<Section>, Int) -> String?
public typealias TitleForFooterInSection = (TableViewSectionedDataSource<Section>, Int) -> String?
public typealias CanEditRowAtIndexPath = (TableViewSectionedDataSource<Section>, IndexPath) -> Bool
public typealias CanMoveRowAtIndexPath = (TableViewSectionedDataSource<Section>, 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
}

效果如下:

swiftui DateComponentsFormatter 本地化 swiftui mvvm_sed_04

四、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上显示

最终实现效果如下:

swiftui DateComponentsFormatter 本地化 swiftui mvvm_RxSwift_05

通过以上的对RxSwift的使用体验,我们会发现,在RxSwift中省略了所有事件的创建,点击事件,编辑事件,按钮事件等等,在哪创建UI,就在哪使用,事件的产生由RxSwift直接提供,UI的展示也可以直接交给RxSwift来赋值。我们需要做的是:数据和UI的相互绑定。

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