2019-08-15 19:07:36 yahibo 阅读数 145

MVVM.png

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

一、登录页面

先看看效果:
login.gif

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

row.gif

展示上没有特别之处。在常规写法中,需要遵循代理并实现代理方法,在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
}

效果如下:

section.gif

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

最终实现效果如下:

search.gif

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

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

2017-08-17 10:45:21 u012352292 阅读数 97

SimpleValidation简单的登录界面

        let usernameValid = usernameOutlet.rx.text.orEmpty
            .map { $0.characters.count >= minimalUsernameLength }
            .shareReplay(1) // without this map would be executed once for each binding, rx is stateless by default

注意这个shareReplay(1)。可以理解为回放。一个事件序列先发生后监听的话就可以误过监听之前的事件。但是shareReplay可以回放最近的N次事件,这样就算后监听也可以获取到想要的值。

这个demo比较简单,其他应该就没什么问题了

2018-06-27 15:25:35 yu_yang92 阅读数 221
疑问: MoyaProvider 只能使用静态常量创建才可以使用Rxswift去订阅返回的消息吗?

// 登录请求(使用该方式时, 该请求始终不执行)
    func login(account: String, password: String) -> Observable<YYAccountLoginResult> {
        let target = MultiTarget(ApiUser.login(phone: account, password: password))

        let provider = MoyaProvider<MultiTarget>()

        return provider.rx.request(target)
            .asObservable()
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .mapObject(type: LoginModel.self)
            .showAPIErrorToast()
            .flatMapLatest({ (model) in
                return Observable.just(YYAccountLoginResult.success(message: "登录成功", data: model))
            })
            
            // 使用以下代码会去执行请求,但是违背了原则,并执行了两次api请求
            // .subscribe()
    }

static let provider = MoyaProvider<MultiTarget>()

// 登录请求(MoyaProvider使用静态常量创建时以下代码执行正常)
    func login(account: String, password: String) -> Observable<YYAccountLoginResult> {
        let target = MultiTarget(ApiUser.login(phone: account, password: password))

        return LoginService.provider.rx.request(target)
            .asObservable()
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .mapObject(type: LoginModel.self)
            .showAPIErrorToast()
            .flatMapLatest({ (model) in
                return Observable.just(YYAccountLoginResult.success(message: "登录成功", data: model))
            })
            
    }


需求: 想通过一些参数创建不同的MoyaProvider; 比如是否出现加载框,该请求是否需要缓存,该请求是否需要验证token是否过期?
原因: 由于每个请求都需要创建MoyaProvider,然后想通过这种方式统一管理
结果造成上述无法访问的问题,由于刚接触rxswift理解不够深入,希望有时间,会的人指教啦,谢谢
/// 在所有url后面拼接key参数
private func endpointMapping<Target: TargetType>(target: Target) -> Endpoint {
    let defaultEndpoint = MoyaProvider.defaultEndpointMapping(for: target)
    let baseUrl = defaultEndpoint.url.appending("?key=\(mobAppKey)")
    return Endpoint(url: baseUrl, sampleResponseClosure: { .networkResponse(200, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: target.headers)
}
private func stubMapping<Target: TargetType>(_: Target) -> StubBehavior {
    // 实时请求api
//    return StubBehavior.never
    // 模拟请求(延迟1秒使用sampleData中的测试数据返回)
    return StubBehavior.delayed(seconds: 1)
}

// 创建moya请求类
    @discardableResult
    static func provider(_ providerType: ProviderType) -> MoyaProvider<MultiTarget> {
        switch providerType {
        case .loding:
            return MoyaProvider<MultiTarget>(endpointClosure: endpointMapping,
                                                     stubClosure: stubMapping,
                                                     plugins: [NetworkLoggerPlugin(verbose: true),
                                                               newworkActivityPlugin,
                                                               RequestLoadingPlugin(true),
                                                               AuthPluginToken()])
        case .noLoading:
            return MoyaProvider<MultiTarget>(endpointClosure: endpointMapping,
                                                     stubClosure: stubMapping,
                                                     plugins: [NetworkLoggerPlugin(verbose: true),
                                                               newworkActivityPlugin,
                                                               RequestLoadingPlugin(false),
                                                               AuthPluginToken()])
        }
    }



2017-10-08 17:44:39 ShmilyCoder 阅读数 1774

RxSwift+Moya之项目实战

RxSwift相关基本介绍和用法可参考:
RxSwift的使用详解01
RxSwift的使用详解02

一. 下面将将进行实战项目

  • 1.登录注册功能
    • 输入用户名要大于6个字符,不然密码不能输入
    • 密码必须大于6个字符,不然重复密码不能输入
    • 重复密码输入必须和密码一样,不然注册按钮不能点击
    • 根据输入的字符是否合法,按钮动态的改变颜色
  • 2.UITableView和搜索SertchBar的应用
    • searchBar根据输入的字体展示包含该字体的cell列表
    • RxSwift实现tableView列表展示
  • 3.Moya+RxSwift实现网络请求

二. Demo地址

下面简单看一下demo的界面

1. 登录注册

登录注册

2. UITableView和SearchBar

UITableView和SearchBar

3. UICollectionView和Moya

UICollectionView和Moya

三. 项目结构和框架

1. 结构

demo是使用的纯MVVM模式,因为RxSwift就是为MVVM而生。不懂MVVM的猿友可参考MVVM模式快速入门

项目结构

2. 项目框架

// Swift三方库
    // Rx
    pod 'RxSwift'  //RxSwift的必备库
    pod 'RxCocoa'  //对 UIKit Foundation 进行 Rx 化
    pod 'RxDataSources'   // 帮助我们优雅的使用tableView的数据源方法

    // 网络请求
    pod 'Moya/RxSwift'  // 为RxSwift专用提供,对Alamofire进行封装的一个网络请求库

    // 图片处理
    pod 'Kingfisher'  //图片处理库

    // 数据解析
    pod 'ObjectMapper'  //json转模型



// OC库
    // MJRefresh
    pod 'MJRefresh'   //MJ上拉下拉刷新
    pod 'SVProgressHUD'  //HUD

四. 注册界面

  • 这里主要使用了Observable的相关知识,不了解的童鞋可参考RxSwift的使用详解01,了解Observable的操作
  • 注册和登录并没有保存已注册的账号和密码, 故登录功能并不完善,后期会在完善,望知晓
  • 下面将针对注册用户名做简单介绍:

1. 首先在model里处理输入字符串的语法法则和字符个数是否符合规范

extension InputValidator {
    //判断字符串是否符合语法法则
    class func isValidEmail(_ email: String) -> Bool {
        let regular = try? NSRegularExpression(pattern: "^\\S+@\\S+\\.\\S+$", options: [])
        if let re = regular {
            let range = NSRange(location: 0, length: email.lengthOfBytes(using: .utf8))
            let result = re.matches(in: email, options: [], range: range)
            return result.count > 0
        }
        return false
    }

    //判断密码字符个数>8
    class func isValidPassword(_ password: String) -> Bool {
        return password.characters.count >= 8
    }

    //判断用户名
    class func validateUserName(_ username: String) -> Result {
        //判断字符个数是否正确
        if username.characters.count < 6 {
            return Result.failure(message: "输入的字符个数不能少于6个字符")
        }

        //账号可用
        return Result.success(message: "账号可用")
    }
}

其中Result是一个返回是否成功的枚举值,可传入字符串变量

enum Result {
    case success(message: String)
    case failure(message: String)
}

2. 根据输入的用户名判断该用户名是否可用

    var usernameObserable: Observable<Result>
    var passwordObserable: Observable<Result>
    var repeatPassObserable: Observable<Result>
    var registerBtnObserable: Observable<Bool>


    init(){
        //检测账号
        usernameObserable = username.asObservable().map({ (username) -> Result in
            return InputValidator.validateUserName(username)
        })
    }    
  • 该返回参数Result,控制器将根据该Result是否成功来改变输入框是否是可编辑状态
  • 初始化方法中,我们对传入的序列进行处理和转换成相对应的Result序列

3. controller逻辑,根据用户名输入改变各控件状态

        //1. 账号判断逻辑
        //1-1. 检测账号
        usernameTextField.rx.text
            .orEmpty // 将String? 类型转为String型
            .bindTo(registerVM.username)
            .addDisposableTo(bag)

        //1-2. 根据账号监听提示字体的状态
        registerVM.usernameObserable
            .bindTo(usernameHintLabel.rx.validationResult)
            .addDisposableTo(bag)

        //1-3. 根据账号监听密码输入框的状态
        registerVM.usernameObserable
            .bindTo(passwordTextField.rx.enableResult)
            .addDisposableTo(bag)
  • 检测输入用户名是否符合规范
  • 根据账号监听提示字体的状态
  • 根据账号监听密码输入框的状态
  • 根据账号监听注册按钮的状态

五. UITableView和SearchBar

  • 该UITableView展示界面并未涉及网络请求
  • 数据来源plist文件
  • 图片为本地图片,可下载demo,在demo中查找图片
  • 选用自定义UITableViewCell,故cell不做介绍
  • model小编这里也不多做介绍,详情可下载demo看具体代码

1. viewModel中的代码逻辑

1-1. 读取plist文件,获取模型数组

fileprivate func getHeroData() -> [HeroModel]{
    // 1.获取路径
    let path = Bundle.main.path(forResource: "heros.plist", ofType: nil)!

    // 2.读取文件内容
    let dictArray = NSArray(contentsOfFile: path) as! [[String : Any]]

    // 3.遍历所有的字典并且转成模型对象
    return dictArray.map({ HeroModel(dict: $0) }).reversed()
}

1-2. seachBar

    lazy var heroVariable: Variable<[HeroModel]> = {
        return Variable(self.getHeroData())
    }()

    var searchText: Observable<String>
    init(searchText: Observable<String>) {
        self.searchText = searchText

        self.searchText.subscribe(onNext: { (str: String) in
            let heros = self.getHeroData().filter({ (hero: HeroModel) -> Bool in
                //过滤
                if str.isEmpty { return true }
                //model是否包含搜索字符串
                return hero.name.contains(str)
            })
            self.heroVariable.value = heros
        }).addDisposableTo(bag)
    }
  • 其中heroVariable是一个数组模型的包装箱,在controller内调用使用前需要asObservable或者asDriver解包装;详细用法可参考:RxSwift的使用详解01
  • searchText搜索框输入的关键字,根据该关键字从数组中过滤出所有包含该关键字的model
  • 对heroVariable重新赋值,发出事件

1-3. RxTableViewController.swift主要代码

1-3-1. searchBar搜索框,输入字符后间隔0.5秒开始搜索

var searchText: Observable<String> {
    //输入后间隔0.5秒搜索,在主线程运行
    return searchBar.rx.text.orEmpty.throttle(0.5, scheduler: MainScheduler.instance)
}

1-3-2. UITableView的设置

    //2.给tableView绑定数据
    //注意: 三个参数:row, model, cell三个顺序不可以搞错, 不需要的可省略 
    heroVM.heroVariable.asDriver().drive(rxTableView.rx.items(cellIdentifier: kCellID, cellType: RxTableViewCell.self)) { (_, hero, cell) in
        cell.heroModel = hero
    }.addDisposableTo(bag)

    // 3.监听UITableView的点击
    rxTableView.rx.modelSelected(HeroModel.self).subscribe { (event: Event<HeroModel>) in
        print(event.element?.name ?? "")
    }.addDisposableTo(bag)
  • 将viewModel中的heroVariable进行解包装,如果是Driver序列,我们这里不使用bingTo,而是使用的Driver,用法和bingTo一模一样。
  • Deriver的监听一定发生在主线程,所以很适合我们更新UI的操作
  • 如需设置delegate的代理
rxTableView.rx.setDelegate(self).addDisposableTo(bag)

然后在实现相应的代理方法即可,如:

extension RxTableViewController: UITableViewDelegate{
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}

六. UICollectionView+Moya+ObjectMapper网络请求和数据处理

  • 与上述UITableView不同的是,这部分将以RxDataSources处理数据源
  • model数组以sections组集合处理
  • 结合Moya进行网络请求
  • 使用ObjectMapper进行json数据转模型

1. 配合ObjectMapper

这里再介绍一下ObjectMapper

class AnchorModel: Mappable {

    var name = ""    //名字
    var pic51 = ""   //头像
    var pic74 = ""   //大图
    var live = 0
    var push = 0
    var focus = 0    //关注量

    required init?(map: Map) {

    }

    func mapping(map: Map) {
        name  <- map["name"]
        pic51 <- map["pic51"]
        pic74 <- map["pic74"]
        live  <- map["live"]
        push  <- map["push"]
        focus <- map["focus"]
    }
}
  • 使用 ObjectMapper ,需要让自己的 Model 类使用 Mappable 协议,这个协议包括两个方法:
required init?(map: Map) {}

func mapping(map: Map) {}
  • 在 mapping 方法中,用 <- 操作符来处理和映射你的 JSON数据
  • 详细的 ObjectMapper 教程可以查看它的 Github 主页,我在这里只做简单的介绍。

2. Moya的使用

  • Moya是基于Alamofire的网络请求库,这里我使用了Moya/Swift,它在Moya的基础上添加了对RxSwift的接口支持。
  • Github上的官方介绍罗列了Moya的一些特点:
    • 编译时检查正确的API端点访问.
    • 使你定义不同端点枚举值对应相应的用途更加明晰.
    • 提高测试地位从而使单元测试更加容易.
  • 接下来我们来说下Moya的使用

2-1. 创建一个枚举API

//请求枚举类型
enum JunNetworkTool {

    case getNewList
    case getHomeList(page: Int)
}

2-2. 为枚举添加扩展

  • 需遵循协议 TargetType
  • 这个协议的Moya这个库规定的协议,可以单击进入相应的文件进行查看
  • 这个协议内的每一个参数(除了validate可不重写)都必须重写,否则会报错
//请求参数
extension JunNetworkTool: TargetType {

    //统一基本的url
    var baseURL: URL {
        return (URL(string: "http://qf.56.com/home/v4/moreAnchor.ios"))!
    }

    //path字段会追加至baseURL后面
    var path: String {
        return ""
    }

    //请求的方式
    var method: Moya.Method {
        return .get
    }

    //参数编码方式(这里使用URL的默认方式)
    var parameterEncoding: ParameterEncoding {
        return URLEncoding.default
    }

    //用于单元测试
    var sampleData: Data {
        return "getList".data(using: .utf8)!
    }

    //将要被执行的任务(请求:request 下载:upload 上传:download)
    var task: Task {
        return .request
    }

    //请求参数(会在请求时进行编码)
    var parameters: [String: Any]? {
        switch self {
        case .getHomeList(let index):
            return ["index": index]
        default:
            return ["index": 1]
        }
    }

    //是否执行Alamofire验证,默认值为false
    var validate: Bool {
        return false
    }
}

2-3. 定义一个全局变量用于整个项目的网络请求

let junNetworkTool = RxMoyaProvider<JunNetworkTool>()

至此,我们就可以使用这个全局变量来请求数据了

3. RxDataSources

  • RxDataSources是以section来做为数据结构来传输,这点很重要,比如:在传统的数据源实现的方法中有一个numberOfSection,我们在很多情况下只需要一个section,所以这个方法可实现,也可以不实现,默认返回的就是1,这给我们带来的一个迷惑点:【tableView是由row来组成的】,不知道在坐的各位中有没有是这么想的呢??有的话那从今天开始就要认清楚这一点,【tableView其实是由section组成的】,所以在使用RxDataSources的过程中,即使你的setion只有一个,那你也得返回一个section的数组出去!!!
  • 传统方式适用于简单的数据集,但不处理需要将复杂数据集与多个部分进行绑定的情况,或者在添加/修改/删除项目时需要执行动画时。而使用RxDataSources时,它很容易写
  • 想了解更多关于RxDataSources的用法,请参考其GitHub主页

3-1. Sections自定义

  • 在我们自定义的Model中创建一个AnchorSection的结构体
  • 并遵循SectionModelType协议,实现相应的协议方法
//MARK: SectionModel
struct AnchorSection {
    // items就是rows
    var items: [Item]

    // 你也可以这里加你需要的东西,比如 headerView 的 title
}

extension AnchorSection: SectionModelType {
    // 重定义 Item 的类型为
    typealias Item = AnchorModel
    init(original: AnchorSection, items: [AnchorSection.Item]) {
        self = original
        self.items = items
    }
}

4. ViewModel

4-1. 自定义协议BaseViewModel

我们知道MVVM思想就是将原本在ViewController的视图显示逻辑、验证逻辑、网络请求等代码存放于ViewModel中,让我们的ViewController瘦身。这些逻辑由ViewModel负责,外界不需要关心,外界只需要结果,ViewModel也只需要将结果给到外界,基于此,我们定义了一个协议


protocol JunViewModelType {
    //associatedtype: 关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定
    associatedtype Input
    associatedtype Output

    //我们通过 transform 方法将input携带的数据进行处理,生成了一个Output
    func transform(input: Input) -> Output
}

4-2. 自定义用于网络请求的刷新状态

  • 根据枚举值的判断,改变collection的刷新状态
//刷新的状态
enum JunRefreshStatus {
    case none
    case beingHeaderRefresh
    case endHeaderRefresh
    case beingFooterRefresh
    case endFooterRefresh
    case noMoreData
}

4-3. 自定义用于继承的BaseViewModel

  • 定义请求数据的页数index
  • 定义input和output的结构体
class BaseViewModel: NSObject {
    // 记录当前的索引值
    var index: Int = 1

    struct JunInput {
        // 网络请求类型
        let category: JunNetworkTool

        init(category: JunNetworkTool) {
            self.category = category
        }
    }

    struct JunOutput {
        // tableView的sections数据
        let sections: Driver<[AnchorSection]>
        // 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)
        let requestCommond = PublishSubject<Bool>()
        // 告诉外界的tableView当前的刷新状态
        let refreshStatus = Variable<JunRefreshStatus>(.none)

        //初始化时,section的数据
        init(sections: Driver<[AnchorSection]>) {
            self.sections = sections
        }
    }
}

4-4. 自定义AnchorViewModel

  • 1) 继承BaseViewModel
class AnchorViewModel : BaseViewModel{
    // 存放着解析完成的模型数组
    let anchorArr = Variable<[AnchorModel]>([])

}
  • 2) 遵循JunViewModelType协议
extension AnchorViewModel: JunViewModelType {
    typealias Input = JunInput
    typealias Output = JunOutput

    func transform(input: AnchorViewModel.JunInput) -> AnchorViewModel.JunOutput {
        let sectionArr = anchorArr.asDriver().map { (models) -> [AnchorSection] in
            // 当models的值被改变时会调用
            return [AnchorSection(items: models)]
        }.asDriver(onErrorJustReturn: [])

        let output = JunOutput(sections: sectionArr)

        output.requestCommond.subscribe(onNext: { (isReloadData) in
            self.index = isReloadData ? 1 : self.index + 1
            //开始请求数据
            junNetworkTool.request(JunNetworkTool.getHomeList(page: self.index))
                .mapObjectArray(AnchorModel.self)
                .subscribe({ (event) in
                    switch event {
                    case let .next(modelArr):
                        self.anchorArr.value = isReloadData ? modelArr : (self.anchorArr.value) + modelArr
                        SVProgressHUD.showSuccess(withStatus: "加载成功")
                    case let .error(error):
                        SVProgressHUD.showError(withStatus: error.localizedDescription)
                    case .completed:
                        output.refreshStatus.value = isReloadData ? .endHeaderRefresh : .endFooterRefresh
                    }
            }).addDisposableTo(bag)
        }).addDisposableTo(bag)

        return output
    }
}
  • sectionArr是将model数组按照section分别存储
  • 当请求回来的anchorArr数据改变的时候, sectionArr随之会发生改变
  • isReloadData用于区分是下拉刷新(true时), 还是上拉加载更多(false时)

5. RxCollectionViewController控制器中

  • 创建数据源RxDataSources
  • 绑定cell
  • 初始化input和output请求
  • 绑定section数据
  • 设置刷新

5-1. 创建数据源RxDataSources

// 创建一个数据源属性,类型为自定义的Section类型
let dataSource = RxCollectionViewSectionedReloadDataSource<AnchorSection>()

5-2. 绑定cell(自定义的cell要提前注册)

dataSource.configureCell = { dataSource, collectionView, indexPath, item in
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: kCollecCellID, for: indexPath) as! RxCollectionViewCell
    cell.anchorModel = item
    return cell
}
  • 以上四个参数的顺序分别为:dataSource, collectionView(或者tableView), indexPath, model, 其对应类型不言而喻,不多做介绍

5-3. 初始化input和output请求

let vmInput = AnchorViewModel.JunInput(category: .getNewList)
let vmOutput = anchorVM.transform(input: vmInput)

5-4. 绑定section数据

//4-1. 通过dataSource和section的model数组绑定数据(demo的用法, 推荐)
vmOutput.sections
    .asDriver()
    .drive(collectionVIew.rx.items(dataSource: dataSource))
    .addDisposableTo(bag)

5-5. 设置刷新

5-5-0. 在controller中初始化刷新状态

collectionVIew.mj_header = MJRefreshNormalHeader(refreshingBlock: {
    vmOutput.requestCommond.onNext(true)
})
collectionVIew.mj_header.beginRefreshing()

collectionVIew.mj_footer = MJRefreshAutoNormalFooter(refreshingBlock: {
    vmOutput.requestCommond.onNext(false)
})

5-5-1. 添加刷新的序列

  • 在JunOutput的结构体中添加刷新序列
  • 我们在进行网络请求并得到结果之后,修改refreshStatus的value为相应的JunRefreshStatus项
  • MJRefre遍会根据该状态做出相应的刷新事件
  • 默认状态为none
// 告诉外界的tableView当前的刷新状态
let refreshStatus = Variable<JunRefreshStatus>(.none)

5-5-2. 外界订阅output的refreshStatus

  • 外界订阅output的refreshStatus,并且根据接收到的值进行相应的操作
  • refreshStatus每次改变都会触发刷新事件
//5. 设置刷新状态
vmOutput.refreshStatus.asObservable().subscribe(onNext: { (status) in
    switch status {
    case .beingHeaderRefresh:
        self.collectionVIew.mj_header.beginRefreshing()
    case .endHeaderRefresh:
        self.collectionVIew.mj_header.endRefreshing()
    case .beingFooterRefresh:
        self.collectionVIew.mj_footer.beginRefreshing()
    case .endFooterRefresh:
        self.collectionVIew.mj_footer.endRefreshing()
    case .noMoreData:                   
        self.collectionVIew.mj_footer.endRefreshingWithNoMoreData()
    default:
        break
    }
}).addDisposableTo(bag)

5-5-3. output提供一个requestCommond用于控制是否请求数据

  • PublishSubject 的特点:即可以作为Observable,也可以作为Observer,说白了就是可以发送信号,也可以订阅信号
  • 当你订阅PublishSubject的时候,你只能接收到订阅他之后发生的事件。subject.onNext()发出onNext事件,对应的还有onError()和onCompleted()事件
// 外界通过该属性告诉viewModel加载数据(传入的值是为了标志是否重新加载)
let requestCommond = PublishSubject<Bool>()

七. 总结

  • 为了研究RxSwift相关知识, 工作之余的时间,差不多一个月了
  • 学习的瓶颈大部分在于网络请求和配合刷新这一模块
  • 文中如出现self循环引用的问题,还望大神多多指正
  • 小编目前也还在初学阶段,文中如出现小错误还望多多指正,如有更好的方法,也希望不吝分享
  • 如果喜欢,可以收藏,也可以在Github上star一下

最后再一次附上Demo地址

参考文献:

没有更多推荐了,返回首页