RxSwift干货技术重塑ios开发学习

RxSwift进阶与实战

2016-12-10  本文已影响9358人  Tangentw

前言

在之前用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)!
    }
}

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.

上一篇下一篇

猜你喜欢

热点阅读