前言
在之前用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
参数,为的是测试,这样子系统就不会请求网络,而是直接通过获取Target
的sampleData
属性。
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.
网友评论
前者是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()
是我哪里理解错了还是怎么?测试代码如下:
```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
有没有空帮我看一下?非常感谢。
根据addDisposableTo(disposeBag),不应该是等到disposeBag被释放了,然后所有注册在disposeBag的订阅者都释放么