RxSwift学习记录二(Moya+HandyJSON+Rx的实战)

概述

上一次写了一些自己入门的一些学习的记录,刚好上周公司有了一个小的售卖机项目需要做一个ios端的,刚好练练手,花了两天时间基本搞的差不多了,写写自己的一些用在项目里的感受,和大家分享一下,希望各位互联网大佬指导一下。


2.gif

实现

项目的一些操蛋逻辑和需求就不详细介绍了,先看看登录界面的实现


QQ20171226-145913.gif

电话号码输入的过程中输入框下面会有随时检测的提醒,正确的时候,点击验证码的按钮状态才是可点击的,当手机和验证码都是符合标准的时候,登录按钮的状态才是可点击的。

  • 上代码

fileprivate func obserState() {

    // 手机号的观察转换
    let phoneState = phoneText.rx.text.orEmpty
                              .map { $0.validateText(.PhoneNumber) }
                              .share(replay: 1)

    // 和提示绑定 以及获取验证码按钮
    phoneState
             .bind(to: phoneAlert.ex_AlertState)
             .disposed(by: dispose)
    phoneState
             .bind(to: getCodeBtn.ex_State)
             .disposed(by: dispose)

    //验证码的转换
    let codeState = phoneCode.rx.text.orEmpty
                             .map { $0.count == 5  }
                             .share(replay: 1)
    //合成手机号和验证码的信号
    let comState = Observable.combineLatest(phoneState, codeState) { $0 && $1 }

    // 与登录按钮进行绑定
    comState.bind(to: applicationBtn.ex_State)
            .disposed(by: dispose)
    //获取验证码的操作 controlEvent 控制事件
   getCodeBtn.rx.tap
                .subscribe({ [weak self] _ in self?.loginVM.getLoginPhoneCode(self?.phoneText.text ?? "" , "login") })
                .disposed(by: dispose)

    // 登录按钮的点击事件
    applicationBtn.rx.tap
        .subscribe({ [weak self] _ in self?.loginVM.bcLogin(self?.phoneText.text ?? "", self?.phoneCode.text ?? "", {
                 self?.presenVC()
             })
        })
        .disposed(by: dispose)

    // 返回按钮的点击事件
    backBtn.rx.tap
        .subscribe({ [weak self] _ in self?.dismiss(animated: true, completion: nil) })
        .disposed(by: dispose)

}

我在代码的每句注释的都很清楚了,就不一一赘述
这里有一个属性绑定的地方,是需要对你绑定的对象类进行Rx扩展

  • 关于按钮的绑定属性
    //自定义可绑定的属性

extension UIButton {
var ex_State:AnyObserver<Bool>{
return Binder(self) { button, state in
button.isEnabled = state
button.backgroundColor = state ? BCBtnEnbelColor : UIColor.lightGray
// button.titleLabel?.textColor = state ? UIColor.white : UIColor.orange //这样设置颜色会有延时误差
//这样设置不会有误差
let color = state ? UIColor.orange : UIColor.lightGray
button.setTitleColor(color, for: .normal)
}.asObserver()
}

. 这里代码的注释中我也写到一个问题,就是我用 button.titleLabel?.textColor设置的时候,会与vc中Rx的判定条件产生一点延时误差不能同步响应,具体原因我也不是很清楚,希望有知道的可以指导一下

  • 关于提示label的绑定属性

//自定义可绑定属性
extension UILabel {
var ex_AlertState: AnyObserver<Bool> {
return Binder(self){ label , exState in
label.textColor = exState ? UIColor.orange : UIColor.red
label.text = exState ? "输入正确" : "请输入正确手机号"
}.asObserver()
}

以上是Rx的一些简单的绑定和常用的功能实现

Moya部分

关于Moya的详细介绍我这里就不说了,官网也都介绍的很详细了,我这边只是说一下我是怎么在项目里用的

  • 上代码

enum BCModuleApi {
case getMobileCode(mobile: String, destination: String) //短信验证码
case goLogin(mobile: String, code: String) //登录
case applyReplenishment(code: String, userId: String, taskNo: Int) //申请补货
case isRobotOpenRequest(actionType: String, identity: Int)//是否正确接收 开机
case doneReplenishment(code: String) //完成补货
case repair(code: String, userId: String, taskNo: Int)//申请维修
case RepairCompleteRequest(code: String ,log: String) //完成维修
// case finishRepairSuccess(identity: String) //App确定售卖机是否已经正确接收完成维修请求
case active(point: String, province: String, city: String, district: String, street: String, room: String, code: String, mobile: String) //设备激活
case activeSuccess(code: String) // App确定售卖机是否已经正确接收激活请求
case unlive(code: String, mobile: String, reason: String)//退服
case regist(name: String, mobile: String, pMobile: String, code: String)//注册
case needAuthen(mobile: String)//用户需要审批的列表
case authenResult(mobile: String, result: String) //审批结果
case synPositionRequest(mobile: String, position: String, type: String) //同步位置
case acceptTask(actionType: String,mobile: String, no: Int)//接受任务 / 拒绝任务
}

extension BCModuleApi: TargetType {
//服务器地址
var baseURL: URL {
return URL.init(string: "#######")!
}

 //各个请求的具体路径
var path: String {
    return ""
}

//请求类型
var method: Moya.Method {
    return .post
}

//请求任务事件(这里附带上参数)
var task: Task {
    switch self {
    case .getMobileCode(let mobile, let destination):
        return .requestParameters(parameters: ["actionType": "getAuthenCode", "mobile": mobile, "destination": destination], encoding: JSONEncoding.default)
    case .goLogin(let mobile, let code):
        return .requestParameters(parameters: ["actionType": "login", "mobile": mobile, "code": code], encoding: JSONEncoding.default)
    case .applyReplenishment(let code, let userId, let taskNo):
        return .requestParameters(parameters: ["actionType": "support","code": code, "userId": userId, "taskNo": taskNo], encoding: JSONEncoding.default)
    case .doneReplenishment(let code):
        return .requestParameters(parameters: ["actionType": "ReplenishmentCompleteRequest", "code": code], encoding: JSONEncoding.default)
    case .repair(let code, let userId, let taskNo):
        return .requestParameters(parameters: ["actionType": "repair", "code": code, "userId": userId, "task": taskNo], encoding: JSONEncoding.default)
    case .active(let point, let province, let city, let district, let street, let room, let code, let mobile):
        return .requestParameters(parameters: ["actionType": "active", "point": point, "province": province, "city": city, "district": district, "street": street, "room": room, "code": code, "mobile":  mobile], encoding: JSONEncoding.default)
    case .unlive(let code, let mobile, let reason):
        return .requestParameters(parameters: ["actionType": "unlive", "code": code, "mobile": mobile, "reason": reason], encoding: JSONEncoding.default)
    case .regist(let name, let mobile, let pMobile, let code):
        return .requestParameters(parameters: ["actionType": "regist", "name": name, "mobile": mobile, "pMobile": pMobile, "code": code], encoding: JSONEncoding.default)
    case .needAuthen(let mobile):
        return .requestParameters(parameters: ["actionType": "needAuthen", "mobile": mobile], encoding: JSONEncoding.default)
    case .authenResult(let mobile, let result):
        return .requestParameters(parameters: ["actionType": "authenResult", "mobile": mobile, "result": result], encoding: JSONEncoding.default)
    case .synPositionRequest(let mobile, let position , let type):
        return .requestParameters(parameters: ["actionType": "synPositionRequest", "mobile": mobile, "position": position, "type": type], encoding: JSONEncoding.default)
    case .isRobotOpenRequest(let actionType, let identity):
        return .requestParameters(parameters: ["actionType": actionType, "identity": identity], encoding: JSONEncoding.default)
    case .acceptTask(let actionType, let mobile, let no):
        return .requestParameters(parameters: ["actionType": actionType, "mobile": mobile, "no": no], encoding: JSONEncoding.default)
    case .activeSuccess(let code):
        return .requestParameters(parameters: ["actionType": "activeSuccess", "code": code], encoding: JSONEncoding.default)
    }
}


//各个请求的参数 (方法废弃)
var parameters: [String: Any]? {
    return nil
}
//这个就是做单元测试模拟的数据,只会在单元测试文件中有作用
var sampleData: Data {
    return "".data(using: String.Encoding.utf8)!
}

//是否执行Alamofire验证
public var validate: Bool {
    return false
}
//请求头 以JSONEncoding.default  形式的请求  JSONEncoding类型创建了一个参数对象的JOSN展示,并作为请求体。编码请求的请求头的Content-Type请求字段被设置为application/json; charset=utf-8
var headers: [String : String]? {
    let headers: HTTPHeaders = [
       "Content-Type": "application/json; charset=utf-8"
    ]
    return headers
}}
  • 这里是我的关于Moya的部分数据请求部分
    由于项目中的接口不太多,所以我就没有根据业务逻辑进行分类进行,都写在了一起
    之前的Moya版本是需要实现的有七个方法,最新版的只需要六个,在代码的注释中我也有标注

//各个请求的参数 (方法废弃)
var parameters: [String: Any]? {
return nil
}
.这个方法被废弃了,现在的参数都是在
var task: Task {
}
方法里统一配置

  • 这里提一下
    我这边的服务器要求的是把参数全部都以一个JSON的形式传到一个路径下,所以

//各个请求的具体路径
var path: String {
return ""
} 这个方法中我都没配置每个的详细路径

以JSOn的形式传给后台在给参数配置的时候参照Task里的方法
需要注意的事,要记得

var headers: [String : String]? {
let headers: HTTPHeaders = [
"Content-Type": "application/json; charset=utf-8"
]
return headers
}} 在此方法中配置一下请求头,不然。。。

1.png

死活获取不到数据,后台那边会显示参数的有问题,乱码。而且奇怪的是,有的接口是会正常的


169278164433053F13854549F5A0E6A5.jpg
2FE902D36E355852F8B6010703661989.jpg
  • 这样我的网络请求层的布置就差不多了接下来是解析的部分

HanyJSON数据解析

//扩展Moya支持HandyJSON的解析
extension ObservableType where E == Response {
public func mapModel<T: HandyJSON>(_ type: T.Type) -> Observable<T> {
return flatMap { response -> Observable<T> in
return Observable.just(response.mapModel(T.self))
}
}
}
extension Response {
func mapModel<T: HandyJSON>(_ type: T.Type) -> T {
let jsonString = String.init(data: data, encoding: .utf8)
if let modelT = JSONDeserializer<T>.deserializeFrom(json: jsonString) {
return modelT
}
return JSONDeserializer<T>.deserializeFrom(json: "{"msg":"请求有误"}")!
}
}

  • 这里我是基于Moya的扩展,支持HandyJSON数据解析,让其结合在一起。需要注意的一点是 我这里的代码

return JSONDeserializer<T>.deserializeFrom(json: "{"msg":"请求有误"}")!
这边是防止有时发生不明的错误,对于jsonString的解析失败,如果此时强制解包的时候程序会Crash,所以我这边用了可选绑定,以及这一句写死JSON传,不过这个要根据你的后台返回的基础数据层级格式写,我后台返回的基础层级结构就是如此的。

  • 下面回到业务当中,看看他们联合在一起产生的化学反应

业务请求

class LoginVM {

 let provider = MoyaProvider<BCModuleApi>()
 let dispose = DisposeBag()

func regist(_ name: String, _ mobile: String, _ pMobile: String, _ code: String, _ success:@escaping ()->()) {
    provider.rx.request(.regist(name: name, mobile: mobile, pMobile: pMobile, code: code))
            .asObservable()
            .retry(3)
            .mapModel(LoginModel.self)
            .subscribe(onNext: { (loginModel) in
                guard loginModel.msg == "success" else {
                    return BCShowMessage.shareMessage.showTipMsg(loginModel.result?.describe ?? "", time: 1)
                }
                BCShowMessage.shareMessage.showTipMsg("注册成功、请等待短信通知再进行登录", time: 1)
                success()
                })
            .disposed(by: dispose)

}
  • 这里是我注册部分的代码
    刚刚对于Moya的扩展,到了现在这边都变成了一句代码

.mapModel(LoginModel.self)

是不是很兴奋,一句代码搞定,JSON -> Model

我的model类是

struct LoginModel: HandyJSON {
var msg: String?
var result: ResultModel?
}
struct ResultModel: HandyJSON {
var describe: String?
var userId: String?
var userName: String?
var userMobile: String?
var leaderId: String?
var leaderName: String?
var leaderMobile: String?
var business: String?
}
Model是必须都要遵守HandyJSON协议的,数据的每层Model都是要遵守的。

多界面数据

  • 对于项目中多个界面同时观察同一个数据的变化,一般情况下我们会以通知的情况,数据发生变化的地方就发起通知,其他需要的地方就接收通知。
    而在Rx中,这中处理变得非常简单,
    只要把你要观察的数据声明成Rx中具有可被观察的属性,并设置单利类,就可以随时在其他地方改变这个数据,并且在你想要观察的地方随时响应。

  • 上代码

static let shareHomeVN = HomeVM() //单利 统一操作 首界面和审查列表的数据

let provider = MoyaProvider<BCModuleApi>()
let dispose = DisposeBag()
let modelList = Variable([ListModel]())

func getNeedAuthenList() {

    let mobile = UserManager.shareManager.userMobile ?? ""
    provider.rx.request(.needAuthen(mobile: mobile))
            .retry(3)
            .asObservable()
            .mapModel(NeedAuthenModel.self)
            .subscribe(onNext: { (needModel) in
                guard needModel.msg == "success" else {
                    return BCShowMessage.shareMessage.showTipMsg(needModel.result?.describe ?? "", time: 1)
                }
                self.modelList.value = needModel.result?.list ?? [ListModel()]

             })
            .disposed(by: dispose)
}

我在首界面和审查界面共享modelList这条数据,只发起一起请求,可以多个界面随时设置随时响应modelList数据。

//绑定 首界面的绑定操作

    homeVM.modelList.asObservable()
          .map { $0.count }
          .subscribe(onNext: { (numText) in
            if numText == 0 {
                self.requestNum.isHidden = true
            }else {
                self.requestNum.text = "\(numText)"
                self.requestNum.isHidden = false
            }
          })
          .disposed(by: dispose)

审查界面的数据绑定
needVM.modelList.asObservable()
.subscribe(onNext: { (listModels) in
self.showAlertState(listModels)
})
.disposed(by: dispose)

审查界面的数据处理
cell.cellOKBtn.rx.tap
.subscribe({ [weak self] _ in self?.needVM.authenResult(model.mobile ?? "", "yes", indexPath.row) })
.disposed(by: dispose)

  • VM的数据处理

func authenResult(_ mobile: String, _ result: String, _ removeIndex: Int) {
provider.rx.request(.authenResult(mobile: mobile, result: result))
.retry(3)
.asObservable()
.mapModel(NeedAuthenModel.self)
.subscribe(onNext: { (needModel) in
guard needModel.msg == "success" else {
return BCShowMessage.shareMessage.showTipMsg(needModel.result?.describe ?? "", time: 1)
}
self.modelList.value.remove(at: removeIndex)
})
.disposed(by: dispose)
}

  • 在每一次的数据操作,共享的界面的绑定都会响应。由于我这边暂时没有数据测试,所以也演示不了。
以上就是我此次用到的部分一些关于Moya+HandyJSON+Rx的应用,项目中的还有其他功能地方就不一一列举了,如果有问题的地方,希望各位大佬能够指导一下,不吝赐教。

共同学习,共同进步!

cxy.jpg

项目代码地址: https://gitee.com/MrBigCat/DX.git

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容