RxSwift进阶与实战

作者: Tangentw | 来源:发表于2016-12-10 09:38 被阅读9358次

前言

在之前用Objective-C语言做项目的时候,我习惯性的会利用MVVM模式去架构项目,在框架ReactiveCocoa的帮助协同下,MVVM架构能够非常优雅地融合与项目中。

Entity 实体

下面进行实体类(Entity)的构建:

 //
//  Entity.swift
//  RxLoginTest
//
//  Created by Tan on 16/7/18.
//  Copyright © 2016年 Tangent. All rights reserved.
//

import UIKit
import RxSwift
import RxCocoa
import Argo
import Moya
import Curry

//  MARK: - User
struct User {
    let name: String
    let userToken: String
}

extension User: Decodable {
    static func decode(json: JSON) -> Decoded<User> {
        return curry(self.init)
            <^> json <| "name"
            <*> json <| "user_token"
    }
}

//  MARK: - ResponseResult
enum ResponseResult {
    case succeed(user: User)
    case faild(message: String)
    
    var user: User? {
        switch self {
        case let .succeed(user):
            return user
        case .faild:
            return nil
        }
    }
}

extension ResponseResult: Decodable {
    init(statusCode: Int, message: String, user: User?) {
        if statusCode == 200 && user != nil {
            self = .succeed(user: user!)
        }else{
            self = .faild(message: message)
        }
    }
    
    static func decode(json: JSON) -> Decoded<ResponseResult> {
        return curry(self.init)
            <^> json <| "status_code"
            <*> json <| "message"
            <*> json <|? "user"
    }
}

//  MARK: - ValidateResult
enum ValidateResult {
    case succeed
    case faild(message: String)
    case empty
}


infix operator ^-^ {}
func ^-^ (lhs: ValidateResult, rhs: ValidateResult) -> Bool {
    switch (lhs, rhs) {
    case  (.succeed, .succeed):
        return true
    default:
        return false
    }
}

//  MARK: - RequestTarget
enum RequestTarget {
    case login(telNum: String, password: String)
}

extension RequestTarget: TargetType {
    var baseURL: NSURL {
        return NSURL(string: "")!
    }
    
    var path: String {
        return "/login"
    }
    
    var method: Moya.Method {
        return .POST
    }
    
    var parameters: [String: AnyObject]? {
        switch self {
        case let .login(telNum, password):
            return ["tel_num": telNum, "password": password]
        default:
            ()
        }
    }
    
    var sampleData: NSData {
        let jsonString = "{\"status_code\":200, \"message\":\"登录成功\", \"user\":{\"name\":\"Tangent\",\"user_token\":\"abcdefg123456\"}}"
        return jsonString.dataUsingEncoding(NSUTF8StringEncoding)!
    }
}

  • User 用户类,登录成功后,后台会返回用户的个人信息,包括用户名称以及用户的登录令牌。
  • ResponseResult 网络请求返回类,枚举类型,成功的话它的关联值是一个用户类型,失败的话它就会有信息字符串关联。它的构造中靠的是状态码来完成,若后台返回的状态码为200,表示登录成功,返回用户,若为其他,表明登录失败,并返回错误信息。这里的decode方法为Argo解析所需实现的。
  • ValidateResult 验证类,如验证电话号码是否格式正确,号码或密码的长度是否达到要求等等,失败的时候会有错误信息相关联。
  • RequestTarget 请求目标,为Moya框架定制的网络请求类。

ViewModelServer 服务

//
//  ViewModelServer.swift
//  RxLoginTest
//
//  Created by Tan on 16/7/18.
//  Copyright © 2016年 Tangent. All rights reserved.
//

import UIKit
import RxCocoa
import RxSwift
import Moya
import Argo

//  MARK: - ValidateServer
class ValidateServer {
    static let instance = ValidateServer()
    
    class func shareInstance() -> ValidateServer {
        return self.instance
    }
    
    let minTelNumCount = 11
    let minPasswordCount = 6
    
    func validateTelNum(telNum: String) -> ValidateResult {
        guard let _ = Int(telNum) else { return .faild(message: "号码格式错误") }
        return telNum.characters.count >= self.minTelNumCount ? .succeed : .faild(message: "号码长度不足")
    }
    
    func validatePassword(password: String) -> ValidateResult {
        return password.characters.count >= self.minPasswordCount ? .succeed : .faild(message: "密码长度不足")
    }
}

//  MARK: - NetworkServer
class NetworkServer {
    static let instance = NetworkServer()
    
    class func shareInstace() -> NetworkServer {
        return self.instance
    }
    
    //  Lazy
    private lazy var provider: RxMoyaProvider = {
        return RxMoyaProvider<RequestTarget>(stubClosure: MoyaProvider.ImmediatelyStub)
    }()
    
    func loginWork(telNum: String, password: String) -> Driver<ResponseResult> {
        return self.provider.request(.login(telNum: telNum, password: password))
            .mapJSON()
            .map { jsonObject -> ResponseResult in
                let decodeResult: Decoded<ResponseResult> = decode(jsonObject)
                return try decodeResult.dematerialize()
            }
            .asDriver(onErrorJustReturn: .faild(message: "网络或数据解析错误!"))
    }
}

在这里有两个服务类,第一个为验证服务类,用于验证用户号码格式以及号码或密码的长度是否达到要求,第二个为网络请求类,用于向后台请求登录,这里要注意的是,RxMoyaProvider一定要被类引用,否则若把它设置为局部变量,请求就不能完成。在构建RxMoyaProvider的时候,我在构造方法中传入了MoyaProvider.ImmediatelyStub这个stubClosure参数,为的是测试,这样子系统就不会请求网络,而是直接通过获取TargetsampleData属性。

ViewModel 视图模型

//
//  ViewModel.swift
//  RxLoginTest
//
//  Created by Tan on 16/7/18.
//  Copyright © 2016年 Tangent. All rights reserved.
//

import UIKit
import RxSwift
import RxCocoa

class ViewModel {
    //  MARK: - Output
    let juhuaShow: Driver<Bool>
    let loginEnable: Driver<Bool>
    let tipString: Driver<String>
    
    init(input: (telNum: Driver<String>, password: Driver<String>, loginTap: Driver<Void>),
         dependency: (validateServer: ValidateServer, networkServer: NetworkServer)) {
        
        let telNumValidate = input.telNum
            .distinctUntilChanged()
            .map { return dependency.validateServer.validateTelNum($0) }
        
        let passwordValidate = input.password
            .distinctUntilChanged()
            .map { return dependency.validateServer.validatePassword($0) }
        
        let validateString = [telNumValidate, passwordValidate]
            .combineLatest { result -> String in
                var validateString = ""
                if case let .faild(message) = result[0] {
                    validateString = "\(message)"
                }
                if case let .faild(message) = result[1] {
                    validateString = "\(validateString) \(message)"
                }
                return validateString
            }
        
        let telNumAndPassWord = Driver.combineLatest(input.telNum, input.password) { ($0, $1) }
        
        let loginString = input.loginTap.withLatestFrom(telNumAndPassWord)
            .flatMapLatest {
                return dependency.networkServer.loginWork($0.0, password: $0.1)
            }
            .map { result -> String in
                switch result {
                case let .faild(message):
                    return "登录失败 \(message)"
                case let .succeed(user):
                    return "登录成功,用户名:\(user.name),标识符:\(user.userToken)"
            }
        }
        
        self.loginEnable = [telNumValidate, passwordValidate]
            .combineLatest { result -> Bool in
                return result[0] ^-^ result[1]
        }
        
        self.juhuaShow = Driver.of(loginString.map{_ in false}, input.loginTap.map{_ in true})
            .merge()
        
        self.tipString = Driver.of(validateString, loginString)
            .merge()
    }
}

ViewModel相对来说比较难搞,毕竟我们要处理好每一个输入输出的关系,灵活进行转变。在这里,没有显式的状态变量,只有对外的输出以及构造时对内的输入,思想就是将输入流进行加工转变成输出流,数据在传输中能够单向传递。

ViewController 视图控制器

//
//  ViewController.swift
//  RxLoginTest
//
//  Created by Tan on 16/7/18.
//  Copyright © 2016年 Tangent. All rights reserved.
//

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {

    @IBOutlet weak var telNumTF: UITextField!
    @IBOutlet weak var passWordTF: UITextField!
    @IBOutlet weak var juhuaView: UIActivityIndicatorView!
    @IBOutlet weak var loginBtn: UIButton!
    @IBOutlet weak var tipLb: UILabel!
    
    private var viewModel: ViewModel?
    private var disposeBag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.viewModel = ViewModel(input: (
                                    self.telNumTF.rx_text.asDriver(),
                                    self.passWordTF.rx_text.asDriver(),
                                    self.loginBtn.rx_tap.asDriver()),
                                   dependency: (
                                    ValidateServer.shareInstance(),
                                    NetworkServer.shareInstace())
                                    )
        //  Binding
        self.viewModel!.juhuaShow
            .drive(self.juhuaView.rx_animating)
            .addDisposableTo(self.disposeBag)
        
        self.viewModel!.loginEnable
            .drive(self.loginBtn.rx_loginEnable)
            .addDisposableTo(self.disposeBag)
        
        self.viewModel!.tipString
            .drive(self.tipLb.rx_text)
            .addDisposableTo(self.disposeBag)
        
    }

}

private extension UIButton {
    var rx_loginEnable: AnyObserver<Bool> {
        return UIBindingObserver(UIElement: self, binding: { (button, bool) in
            self.enabled = bool
            if bool {
                button.backgroundColor = UIColor.greenColor()
            }else{
                button.backgroundColor = UIColor.redColor()
            }
        }).asObserver()
    }
}

在这里,我们构建好ViewModel,将输入以及视图模型依赖的服务传入ViewModel构造方法中,并在下面把ViewModel的输入去驱动UI视图。


到这里,我们的实战项目就搞定啦~
如果你想下载项目源代码,可以Click入我的GitHub:RxSwiftLoginTest GitHub-Tangent

参考资料

本文主要参考RxSwift官方文档以及官方给出的一些实例,详情请访问RxSwift在GitHub上的栏目:
RxSwift GitHub.

相关文章

网友评论

  • 走停2015_iOS开发:我发现概念的东西我看的懂 一到项目实践我就看不懂了
  • 天涯人1104:您好,请问一下,自定义控件怎么添加 rx啊
    天涯人1104:@Tangentw 没看到,谢谢提醒
    Tangentw:@天涯人1104 方法有很多,RxSwift仓库中的Example项目就有例子,比如说对UIImagePickerController的Rx封装。
  • FongG:作者你好,我想请教一个问题,关于譬如textField.rx.text和textField.rx.text.orEmpty。

    前者是ControlProperty<String?>类型,后者是ControlProperty<String>。

    ControlProperty类型有一个扩展,是
    extension ControlProperty {
    /// Converts `ControlProperty` to `Driver` trait.
    ///
    /// `ControlProperty` already can't fail, so no special case needs to be handled.
    public func asDriver() -> Driver<E> {
    return self.asDriver { (error) -> Driver<E> in
    #if DEBUG
    rxFatalError("Somehow driver received error from a source that shouldn't fail.")
    #else
    return Driver.empty()
    #endif
    }
    }
    }

    我想请问为什么textField.rx.text.asDriver() 语法是错误的,必须先转化为textField.rx.text.orEmpty.asDriver()
    黑暗中的孤影:@Jin_先生 这应该是API有修改
    FongG:@TangentW 但这样写就会报错,一定得加.orEmpty
    Tangentw:你好,tf.rx.text.asDriver()语法是没有问题的
  • FongG:感谢作者无私分享:pray:
  • 1c7530ba374e:实际测了一下,`shareReplayLatestWhileConnected()` 方法并不能阻止 `map()` 方法多次调用,但`shareReplay(bufferSize:)` 方法可以。
    是我哪里理解错了还是怎么?测试代码如下:
    ```swift
    let disposeBag = DisposeBag()

    let one = Observable.just(1)
    .map { value -> Int in
    print("计算");
    return value * value
    }
    // .shareReplay(1)
    .shareReplayLatestWhileConnected()

    one
    .subscribe(onNext: {
    print("订阅者1 : \($0)")
    })
    .addDisposableTo(disposeBag)

    one
    .subscribe(onNext: {
    print("订阅者2 : \($0)")
    })
    .addDisposableTo(disposeBag)
    ```
    Output:
    计算
    订阅者1 : 1
    计算
    订阅者2 : 1

    有没有空帮我看一下?非常感谢。
    Tangentw:@Jin_先生 虽说Completed事件以及Error事件都能结束一个流,并且释放流所占用的资源,但是,并不是所有的流最终都会发出Completed或者Error,所以需要一个自动释放的机制。
    FongG:@CSwater “ 接收到 Completed 信号,序列结束,释放订阅者,清空缓存区的最新元素” 。
    根据addDisposableTo(disposeBag),不应该是等到disposeBag被释放了,然后所有注册在disposeBag的订阅者都释放么
    1c7530ba374e:问题已经解决。 just(1) 序列等于 .onNext(1) -> Completed。 接收到 Completed 信号,序列结束,释放订阅者,清空缓存区的最新元素。下一次订阅开始,又重新执行一遍。
  • __________mo:必须赞一个
  • 鬼谷门生:你好,你的数据与tableView绑定的好像是本地数据,那么要是我在viewModel请求网络数据成功后又怎么和tableView绑定呢?大致应该怎么绑定才能时时监听网络请求成功后数据的改变,还有RxSwift对cell的高度做了扩展了么?
  • 悟_空:刚看了一遍 没有看懂,准备在看两遍,项目中再用,不会不行啊
  • 39749980faf8:看了一个多小时看到项目实战那里,这是我看到的又一篇关于RxSwift写得很好的文章了。感觉对RxSwift理解总算是清晰了些,休息下继续看实战部分。感谢作者精彩无私讲解。
  • _八阿哥:大哥,请问一下,你有没有写RxSwift基础的讲解呀
    _八阿哥:@TangentW :sob: 全英文的都看不明白,而且playground用起来卡成狗
    Tangentw:@_八阿哥 这方面我没有写喔,基础的话我觉得看下RxSwift官方项目的playground文件以及研究下里面的例子就行了:yum:
  • da27c260cc85:可以写个介绍你怎么封装moya的文章么?
    da27c260cc85:@TangentW 哔,学生卡
    Tangentw:@ArthurChi 可以的,过几天我会发出来:yum:
  • da27c260cc85:你好,想请教一下,怎么把微博的SDK用rxswift包装一下呢?
    Tangentw:可以给我看下几个方法吗
    da27c260cc85:@TangentW 那个SDK使用代理,但是全都是类方法,感觉无从下手
    Tangentw:我没用过微博的SDK,可以更详细地描述下吗?
  • MrMessy:写的非常好,值得参考。

本文标题:RxSwift进阶与实战

本文链接:https://www.haomeiwen.com/subject/kowymttx.html