前言
在之前用Objective-C
語言做項目的時候,我習慣性的會利用MVVM
模式去架構(gòu)項目按傅,在框架ReactiveCocoa
的幫助協(xié)同下,MVVM
架構(gòu)能夠非常優(yōu)雅地融合與項目中眶熬。

ReactiveCocoa是具有響應式以及函數(shù)式編程特點的第三方開源框架侵佃,它可以在
MVVM
架構(gòu)模式中充當著View(視圖)
層與ViewModel(視圖模型)
層之間的Binder(綁定者)
角色,實現(xiàn)兩個層之間的同步更新踱蛀。在ReactiveCocoa
的世界中劫笙,數(shù)據(jù)與屬性的改變、視圖的操作反饋星岗、方法的調(diào)用等都可以被監(jiān)聽并抽象轉(zhuǎn)換成事件流填大,封裝在Signal(信號)
中,我們通過對Signal
的Subscribe(訂閱)
就能獲取到其中的事件流俏橘,并進行相應的操作允华。
近期這段時間,我重新折騰起了Swift
寥掐。在我剛剛初步掌握Swift
語言的時候靴寂,也就用它做了一個以MVC
為架構(gòu)模式的較為簡單的項目而已,后面寫到一半左右就爛尾了召耘,轉(zhuǎn)為用Objective-C
去折騰另一個較為龐大的項目百炬。在幾天前搞起Swift
時,我思考過污它,有沒有一種解決方案能夠在Swift
中像ReactiveCocoa
一樣能夠優(yōu)雅地實現(xiàn)MVVM
架構(gòu)呢剖踊?查閱相關(guān)資料,我了解到ReactiveCocoa
也能在Swift
環(huán)境下使用衫贬,也認識了另一個第三方框架 —— RxSwift德澈,在對其的學習與實踐中,我也越來越中意這貨了固惯。

RxSwift為
ReactiveX(Reactive Extensions)
旗下的Swift
語言庫梆造,提供了Swift
平臺上進行響應式編程的解決方案。Rx
的重要角色為Observable(被觀察者)
和Observer(觀察者)
葬毫,Observable
類似于ReactiveCocoa
中的Signal
镇辉,里面裝有事件流
屡穗,供Observer
訂閱。事件流
在Rx
中與ReactiveCocoa
一樣具有三類:Next
忽肛、Error
鸡捐、Completed
,代表著繼續(xù)事件麻裁、錯誤事件箍镜、完成事件。我們在使用RxSwift
進行iOS開發(fā)時煎源,通常會引入另外一個庫:RxCocoa
色迂,這個庫將UIKit
以及Foundation
框架中許多成員,如視圖(View)手销、控制事件(Control Event)歇僧、鍵值觀察(KVO)、通知(Notification)等等進行與RxSwift
接入的擴展锋拖,將Rx
與iOS API無縫連接诈悍。
本文主要針對RxSwift
闡述它的進階使用,以及在最后結(jié)合MVVM
項目實戰(zhàn)來鞏固知識點兽埃。作為一篇總結(jié)我自己對RxSwift
學習的文章侥钳。
有關(guān)RxSwift
的基礎(chǔ)教程可前往此項目的GitHub倉庫中下載,里面會有個專門介紹基礎(chǔ)使用的playground
工程文件: GitHub: RxSwift
進階講解
bindTo
bindTo
為ObservableType
協(xié)議的幾個重載方法(Observable
也會實現(xiàn)ObservableType
協(xié)議)柄错。顧名思義舷夺,它會將某個東東與一個可觀察者進行綁定,也就是說售貌,當這個可觀察者的事件流中有事件“流過”(有事件元素發(fā)送)给猾,被綁定的這個東東就會被刺激到,進而進行相關(guān)的操作颂跨。
在這里敢伸,有一個用的比較多的是重載方法為bindTo<O : ObserverType where O.E == E>(observer: O) -> Disposable
,這個方法有一個參數(shù)恒削,從方法泛型的聲明中可以得知池颈,參數(shù)的類型為一個觀察者類型,且這個觀察者能夠接受到的事件流元素的類型要跟被觀察者的一樣(O.E == E)蔓同。這個方法意圖就是將一個被觀察者與一個指定的觀察者進行綁定饶辙,被觀察者事件流中發(fā)出的所有事件元素都會讓觀察者接收蹲诀。
在MVVM
架構(gòu)模式中斑粱,此方法主要用于視圖(View)層跟視圖模型(ViewModel)層或視圖層跟視圖層的綁定,這里舉個栗子:
textField.rx_text
.bindTo(label.rx_text)
.addDisposableTo(disposeBag)
其中脯爪,UITextField的rx_text屬性為ControlProperty類型则北,實現(xiàn)了ControlPropertyType矿微,所以不僅是觀察者類型,還是被觀察者類型尚揣,UILabel中的rx_text只是單純的觀察者類型涌矢。
bindTo
的另外一個用得比較多的重載方法為:bindTo(variable: RxSwift.Variable<Self.E>) -> Disposable
,這個方法將一個被觀察者與一個Variable(變量)
綁定在一起快骗,這個變量的元素類型跟被觀察者的事件元素類型一致娜庇。此方法作用就是把從被觀察者事件流中發(fā)射出的事件元素存入變量中,在這里不做演示方篮。
關(guān)于bindTo
的其他重載方法在這里就不完全闡述了名秀,剩下的主要是用于對函數(shù)的綁定(還有針對柯里化的函數(shù))。
UIBindingObserver
現(xiàn)在介紹的這個東東就跟上面說的被觀察者類型的bindTo
方法密切相關(guān)了藕溅。
UIBindingObserver
匕得,名字就告訴了我們它是一個觀察者,用于對UI的綁定巾表,我這里通過一個例子來講解它:
// MARK: - 綁定方法
func binding() {
textField.rx_text
.bindTo(label.rx_sayHelloObserver)
.addDisposableTo(disposeBag)
}
// MARK: - 視圖控件擴展
private extension UILabel {
var rx_sayHelloObserver: AnyObserver<String> {
return UIBindingObserver(UIElement: self, binding: { (label, string) in
label.text = "Hello \(string)"
}).asObserver()
}
}
上面的代碼中汁掠,我在視圖控制器ViewController所在的Swift文件中創(chuàng)建了一個私有的UILabel
擴展,并在擴展中定義了一個只讀計算屬性集币,屬性的類型為AnyObserver<String>
考阱,為一個事件元素是String
的觀察者類型。當獲取這個屬性值的時候鞠苟,就返回了與特定UIBindingObserver
關(guān)聯(lián)的觀察者羔砾。
現(xiàn)在我們來看一下UIBindingObserver
的構(gòu)造方法:
init(UIElement: UIElementType, binding: (UIElementType, Value) -> Void)
方法的第一個參數(shù)就是傳入一個要被綁定的視圖的實例,由于現(xiàn)在是在UILabel
的擴展中偶妖,所以這里我傳入了self
,代表UILabel
自己扼鞋;構(gòu)造方法的第二個參數(shù)為一個無返回值的閉包類型溃槐,閉包的參數(shù)其一就是被綁定了的視圖,其二就是由綁定的被觀察者中所發(fā)射出來的事件元素。通過這個閉包抚恒,我們能夠?qū)⒁晥D中的某些屬性根據(jù)相應的事件元素而進行改變表鳍,如例子中label.text = "Hello \(string)"
屯蹦。當我們執(zhí)行例子中的binding
函數(shù)進行綁定后,TextField
中的字符串每經(jīng)過修改,Label
中的文字總會實時更新毫玖,并在字符串前面加上Hello
励背。
在RxCocoa
框架中,某些地方也用到了UIBindingObserver
绩郎,如UILable
中的rx_text
:
public var rx_text: AnyObserver<String> {
return UIBindingObserver(UIElement: self) { label, text in
label.text = text
}.asObserver()
}
Driver
Driver
從名字上可以理解為驅(qū)動
(我自己會親切地把它叫做"老司機")潘鲫,在功能上它類似被觀察者(Observable),而它本身也可以與被觀察者相互轉(zhuǎn)換(Observable: asDriver, Driver: asObservable)肋杖,它驅(qū)動著一個觀察者溉仑,當它的事件流中有事件涌出時,被它驅(qū)動著的觀察者就能進行相應的操作状植。一般我們會將一個Observable
被觀察者轉(zhuǎn)換成Driver
后再進行驅(qū)動操作:
我們沿用上面例子中的UILabel
私有擴展浊竟,并修改下binding
方法:
func binding() {
textField.rx_text
.asDriver()
.drive(label.rx_sayHelloObserver)
.addDisposableTo(disposeBag)
}
可見,Driver
的drive
方法與Observable
的方法bindTo
用法非常相似津畸,事實上振定,它們的作用也是一樣,說白了就是被觀察者與觀察者的綁定肉拓。那為什么RxSwift
的作者又搞出Driver
這么個東西來呢吩案?
其實,比較與Observable
帝簇,Driver
有以下的特性:
- 它不會發(fā)射出錯誤(Error)事件
- 對它的觀察訂閱是發(fā)生在主線程(UI線程)的
- 自帶
shareReplayLatestWhileConnected
下面就圍繞著這三個特性一一研究下:
-
當你將一個
Observable
轉(zhuǎn)換成Driver
時徘郭,用到的asDriver
方法有下面幾個重載:asDriver(onErrorJustReturn onErrorJustReturn: Self.E) asDriver(onErrorDriveWith onErrorDriveWith: RxCocoa.Driver<Self.E>) asDriver(onErrorRecover onErrorRecover: (error: ErrorType) -> RxCocoa.Driver<Self.E>)
從這三個重載方法中可看出,當我們要將有可能會發(fā)出錯誤事件的Observable
轉(zhuǎn)換成Driver
時丧肴,必須要先將所有可能發(fā)出的錯誤事件濾除掉残揉,從而使得Driver
不可能會發(fā)射出錯誤的事件。
- 在
Observable
中假如你要進行限流芋浮,你要用到方法throttle(dueTime: RxSwift.RxTimeInterval, scheduler: SchedulerType)
抱环,方法的第一個參數(shù)是兩個事件之間的間隔時間壳快,第二個參數(shù)是一個線程的有關(guān)類,如我要在主線程中镇草,我可以傳入MainScheduler.instance
眶痰。而在Driver
中我們要限流,調(diào)用的是throttle(dueTime: RxSwift.RxTimeInterval)
梯啤,只配置事件的間隔時間竖伯,而它默認會在主線程中進行。 - 一般我們在對
Observable
進行map
操作后因宇,我們會在后面加上shareReplay(1)
或shareReplayLatestWhileConnected
七婴,以防止以后被觀察者被多次訂閱觀察后,map
中的語句會多次調(diào)用:
let rx_textChange = textField.rx_text
.map { return "Good \($0)" }
.shareReplay(1)
rx_textChange
.subscribeNext { print("1 -- \($0)") }
.addDisposableTo(disposeBag)
rx_textChange
.subscribeNext { print("2 -- \($0)") }
.addDisposableTo(disposeBag)
在Driver
中察滑,框架已經(jīng)默認幫我們加上了shareReplayLatestWhileConnected
打厘,所以我們也沒必要再加上"replay"相關(guān)的語句了。
從這些特性可以看出贺辰,Driver
是一個專門針對于UI的特定可觀察者類户盯。并不是說對UI進行相應綁定操作不能使用純粹的Observable
,但是饲化,Driver
已經(jīng)幫我們省去了好多的操作莽鸭,讓我們對UI的綁定更加的高效便捷。所以滓侍,對UI視圖的綁定操作蒋川,我們首選“老司機”Driver
。
DisposeBag
當一個Observable(被觀察者)
被觀察訂閱后撩笆,就會產(chǎn)生一個Disposable
實例捺球,通過這個實例,我們就能進行資源的釋放了夕冲。
對于RxSwift
中資源的釋放氮兵,也就是解除綁定、釋放空間歹鱼,有兩種方法泣栈,分別是顯式釋放以及隱式釋放:
- 顯式釋放 可以讓我們在代碼中直接調(diào)用釋放方法進行資源的釋放,如下面的實例:
let dispose = textField.rx_text
.bindTo(label.rx_sayHelloObserver)
dispose.dispose()
這個例子只是為了更明朗地說明顯式釋放方法而已弥姻,實際上并不會這樣寫南片。
-
隱式釋放 則通過
DisposeBag
來進行,它類似于Objective-C ARC
中的自動釋放池
機制庭敦,當我們創(chuàng)建了某個實例后疼进,會被添加到所在線程的自動釋放池中,而自動釋放池會在一個RunLoop
周期后進行池子的釋放與重建秧廉;DisposeBag
對于RxSwift
就像自動釋放池
一樣伞广,我們把資源添加到DisposeBag
中拣帽,讓資源隨著DisposeBag
一起釋放。如下實例:
let disposeBag = DisposeBag()
func binding() {
textField.rx_text
.bindTo(label.rx_sayHelloObserver)
.addDisposableTo(self.disposeBag)
}
方法addDisposableTo
會對DisposeBag
進行弱引用嚼锄,所以這個DisposeBag
要被實例引用著减拭,一般可作為實例的成員變量,當實例被銷毀了区丑,成員DisposeBag
會跟著銷毀拧粪,從而使得RxSwift
在此實例上綁定的資源得到釋放。
對于UITableViewCell
跟UICollectionViewCell
來說刊苍,DisposeBag
也能讓cell在重用前釋放掉之前被綁定的資源:
class TanTableViewCell: UITableViewCell {
var disposeBag: DisposeBag?
var viewModel: TanCellViewModel? {
didSet {
let disposeBag = DisposeBag()
viewModel?.title
.drive(self.textLabel!.rx_text)
.addDisposableTo(disposeBag)
self.disposeBag = disposeBag
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.disposeBag = nil
}
}
DataSource
這里主要講解的是RxCocoa
框架中帶有的對于UITableView
以及UICollectionView
數(shù)據(jù)源的解決方案既们,在GitHub中也有一個開源小庫RxDataSource
濒析,在這里我就不再研究了正什,有興趣的朋友可以去看看:GitHub RxDataSource。
我這里用一個例子來展示下RxCocoa
中的簡單UITableView
數(shù)據(jù)源:
class TanViewController: UIViewController {
var disposeBag = DisposeBag()
let data = [TanCellViewModel(title: "One"), TanCellViewModel(title: "Two"), TanCellViewModel(title: "Three")]
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.tableView)
self.tableView.frame = self.view.bounds
self.binging()
}
private func binging() {
Observable.just(self.data)
.asDriver(onErrorJustReturn: [])
.drive(self.tableView.rx_itemsWithCellIdentifier(TanTableViewCell.CELL_IDENTIFIER, cellType: TanTableViewCell.self)) { (_, viewModel, cell) in
cell.viewModel = viewModel
}
.addDisposableTo(self.disposeBag)
}
// MARK: - Lazy
private var tableView: UITableView = {
let tableView = UITableView(frame: CGRectZero, style: .Plain)
tableView.registerClass(TanTableViewCell.self, forCellReuseIdentifier: TanTableViewCell.CELL_IDENTIFIER)
return tableView
}()
}
如上号杏,我們能夠?qū)?shù)據(jù)封裝在Observable
中婴氮,然后在吧Observable
綁定到UITableView
中,通過UITableView
的方法rx_itemsWithCellIdentifier
盾致,我們就能夠進行數(shù)據(jù)跟Cell的一一對應配置主经。
到此,UITableView
的數(shù)據(jù)源就設(shè)置好了庭惜。UICollectionView
的數(shù)據(jù)源設(shè)置跟UITableView
差不多罩驻,在這里就不再作例子了。
項目實戰(zhàn)
下面就是重頭戲了护赊,我將通過折騰出一個小項目來演示RxSwift
的使用惠遏,包括基礎(chǔ)以及進階的內(nèi)容,首先來設(shè)定下這個項目:
說簡單點骏啰,就是做一個登錄界面(萬能Demo)??节吮,輸入用戶號碼跟密碼,點擊登錄按鈕判耕,即可登錄獲取數(shù)據(jù)透绩。??
說復雜點,我們要完成下面的要求:
- 用戶號碼輸入框要判斷用戶輸入的是否全是數(shù)字壁熄,若格式不正確帚豪,提示用戶格式錯誤。
- 號碼輸入框輸入的數(shù)字最少要有11位草丧,密碼輸入框輸入的字符串長度最少要有6位狸臣。
- 要滿足上面的兩條要求,登錄按鈕才可以點擊方仿。
- 登錄按鈕點擊后進行登錄固棚,界面顯示正在轉(zhuǎn)動的等待視圖统翩,當接收到后臺數(shù)據(jù)時,等待視圖消失此洲。
- 解析后臺返回的數(shù)據(jù)厂汗,并把數(shù)據(jù)呈現(xiàn)到界面中。
在這個項目中呜师,我還是使用熟悉的MVVM
架構(gòu)模式娶桦。在開干之前我首先要說幾點:
-
RxSwift
中的ViewModel
是沒有什么明確的狀態(tài)的,它的輸出由輸入決定汁汗,可以這么說衷畦,我們要使用RxSwift
將ViewModel
中的外界輸入(UI觸發(fā)、外界事件)轉(zhuǎn)換成輸出知牌,再由這些輸出去驅(qū)動UI界面祈争,并且,ViewModel
做的是轉(zhuǎn)換角寸,我們不能夠在其中對某個Observable
進行訂閱操作菩混,所以,在ViewModel
中我們是看不到addDisposableTo
的扁藕。 - 我對比了一下由
ReactiveCocoa
與RxSwift
實現(xiàn)的ViewModel
沮峡,發(fā)現(xiàn)使用ReactiveCocoa
實現(xiàn)的ViewModel
中會有比較多的明確狀態(tài)變量,比如說現(xiàn)在實現(xiàn)的是登錄的界面亿柑,在ReactiveCocoa
的ViewModel
中我們會看到有"userName"邢疙、"passWord"等等之類的狀態(tài)變量,它是由ReactiveCocoa
將其與UI視圖屬性相綁定的:RAC(self.viewModel, userName) = userNameTextField.rac_textSignal;
望薄,而在RxSwift
實現(xiàn)的ViewModel
疟游,就不會看到這些狀態(tài)變量了,有的是驅(qū)動外界UI的輸出Driver
式矫,個人認為RxSwift
實現(xiàn)ViewModel
的宗旨是將外界視圖的輸入經(jīng)過轉(zhuǎn)變產(chǎn)生輸出乡摹,在讓輸出去驅(qū)動回UI視圖,所以我在構(gòu)建ViewModel
類的時候采转,會在它的構(gòu)造方法中開設(shè)一個接收輸入的參數(shù)聪廉,其次就在后面的控制器綁定中將ViewModel
的輸出進行訂閱,驅(qū)動視圖層故慈。 - 這個項目我使用的第三方庫有
RxSwift
板熊、RxCocoa
、Moya
察绷、Argo
干签、Curry
,前面兩個在上面有說到拆撼;Moya
是一款Swift
語言的網(wǎng)絡(luò)請求框架容劳,它是另一款網(wǎng)絡(luò)請求框架Alamofire
的再度封裝喘沿,它有基于RxSwift
的擴展,能與RxSwift
無縫對接竭贩;Argo
是一款小巧的JSON
解析庫蚜印,函數(shù)柯里化(Currying)
庫Curry
配合著它一起使用,而且留量,Argo
的解析語法非常新穎奇特窄赋,用著感覺非常過癮!
敲代碼走起~
界面
在Storyboard
中布局好登錄界面楼熄,分別有用戶電話號碼的輸入框忆绰、用戶密碼輸入框、等待視圖(菊花)可岂、提示視圖(用于提醒輸入的錯誤恃轩,以及登錄的狀態(tài))社露、登錄按鈕:
Entity 實體
下面進行實體類(Entity)的構(gòu)建:
//
// 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 網(wǎng)絡(luò)請求返回類,枚舉類型萎馅,成功的話它的關(guān)聯(lián)值是一個用戶類型双戳,失敗的話它就會有信息字符串關(guān)聯(lián)。它的構(gòu)造中靠的是狀態(tài)碼來完成糜芳,若后臺返回的狀態(tài)碼為
200
飒货,表示登錄成功,返回用戶峭竣,若為其他塘辅,表明登錄失敗,并返回錯誤信息皆撩。這里的decode
方法為Argo
解析所需實現(xiàn)的扣墩。 - ValidateResult 驗證類,如驗證電話號碼是否格式正確扛吞,號碼或密碼的長度是否達到要求等等呻惕,失敗的時候會有錯誤信息相關(guān)聯(lián)。
-
RequestTarget 請求目標滥比,為
Moya
框架定制的網(wǎng)絡(luò)請求類亚脆。
ViewModelServer 服務(wù)
//
// 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: "網(wǎng)絡(luò)或數(shù)據(jù)解析錯誤!"))
}
}
在這里有兩個服務(wù)類盲泛,第一個為驗證服務(wù)類濒持,用于驗證用戶號碼格式以及號碼或密碼的長度是否達到要求键耕,第二個為網(wǎng)絡(luò)請求類,用于向后臺請求登錄柑营,這里要注意的是郁竟,RxMoyaProvider
一定要被類引用,否則若把它設(shè)置為局部變量由境,請求就不能完成棚亩。在構(gòu)建RxMoyaProvider
的時候,我在構(gòu)造方法中傳入了MoyaProvider.ImmediatelyStub
這個stubClosure
參數(shù)虏杰,為的是測試讥蟆,這樣子系統(tǒng)就不會請求網(wǎng)絡(luò),而是直接通過獲取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
相對來說比較難搞笛钝,畢竟我們要處理好每一個輸入輸出的關(guān)系质况,靈活進行轉(zhuǎn)變。在這里玻靡,沒有顯式的狀態(tài)變量结榄,只有對外的輸出以及構(gòu)造時對內(nèi)的輸入,思想就是將輸入流進行加工轉(zhuǎn)變成輸出流囤捻,數(shù)據(jù)在傳輸中能夠單向傳遞臼朗。
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()
}
}
在這里,我們構(gòu)建好ViewModel
蝎土,將輸入以及視圖模型依賴的服務(wù)傳入ViewModel
構(gòu)造方法中视哑,并在下面把ViewModel
的輸入去驅(qū)動UI視圖。
到這里誊涯,我們的實戰(zhàn)項目就搞定啦~
如果你想下載項目源代碼挡毅,可以Click入我的GitHub:RxSwiftLoginTest GitHub-Tangent
參考資料
本文主要參考RxSwift
官方文檔以及官方給出的一些實例,詳情請訪問RxSwift
在GitHub上的欄目:
RxSwift GitHub.