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