前言
在之前用Objective-C
語言做項目的時候,我習慣性的會利用MVVM
模式去架構項目,在框架ReactiveCocoa
的幫助協同下,MVVM
架構能夠非常優雅地融合與項目中。

ReactiveCocoa是具有響應式以及函數式編程特點的第三方開源框架,它可以在
MVVM
架構模式中充當著View(視圖)
層與ViewModel(視圖模型)
層之間的Binder(綁定者)
角色,實現兩個層之間的同步更新。在ReactiveCocoa
的世界中,數據與屬性的改變、視圖的操作反饋、方法的調用等都可以被監聽并抽象轉換成事件流,封裝在Signal(信號)
中,我們通過對Signal
的Subscribe(訂閱)
就能獲取到其中的事件流,并進行相應的操作。
近期這段時間,我重新折騰起了Swift
。在我剛剛初步掌握Swift
語言的時候,也就用它做了一個以MVC
為架構模式的較為簡單的項目而已,后面寫到一半左右就爛尾了,轉為用Objective-C
去折騰另一個較為龐大的項目。在幾天前搞起Swift
時,我思考過,有沒有一種解決方案能夠在Swift
中像ReactiveCocoa
一樣能夠優雅地實現MVVM
架構呢?查閱相關資料,我了解到ReactiveCocoa
也能在Swift
環境下使用,也認識了另一個第三方框架 —— RxSwift,在對其的學習與實踐中,我也越來越中意這貨了。

RxSwift為
ReactiveX(Reactive Extensions)
旗下的Swift
語言庫,提供了Swift
平臺上進行響應式編程的解決方案。Rx
的重要角色為Observable(被觀察者)
和Observer(觀察者)
,Observable
類似于ReactiveCocoa
中的Signal
,里面裝有事件流
,供Observer
訂閱。事件流
在Rx
中與ReactiveCocoa
一樣具有三類:Next
、Error
、Completed
,代表著繼續事件、錯誤事件、完成事件。我們在使用RxSwift
進行iOS開發時,通常會引入另外一個庫:RxCocoa
,這個庫將UIKit
以及Foundation
框架中許多成員,如視圖(View)、控制事件(Control Event)、鍵值觀察(KVO)、通知(Notification)等等進行與RxSwift
接入的擴展,將Rx
與iOS API無縫連接。
本文主要針對RxSwift
闡述它的進階使用,以及在最后結合MVVM
項目實戰來鞏固知識點。作為一篇總結我自己對RxSwift
學習的文章。
有關RxSwift
的基礎教程可前往此項目的GitHub倉庫中下載,里面會有個專門介紹基礎使用的playground
工程文件: GitHub: RxSwift
進階講解
bindTo
bindTo
為ObservableType
協議的幾個重載方法(Observable
也會實現ObservableType
協議)。顧名思義,它會將某個東東與一個可觀察者進行綁定,也就是說,當這個可觀察者的事件流中有事件“流過”(有事件元素發送),被綁定的這個東東就會被刺激到,進而進行相關的操作。
在這里,有一個用的比較多的是重載方法為bindTo<O : ObserverType where O.E == E>(observer: O) -> Disposable
,這個方法有一個參數,從方法泛型的聲明中可以得知,參數的類型為一個觀察者類型,且這個觀察者能夠接受到的事件流元素的類型要跟被觀察者的一樣(O.E == E)。這個方法意圖就是將一個被觀察者與一個指定的觀察者進行綁定,被觀察者事件流中發出的所有事件元素都會讓觀察者接收。
在MVVM
架構模式中,此方法主要用于視圖(View)層跟視圖模型(ViewModel)層或視圖層跟視圖層的綁定,這里舉個栗子:
textField.rx_text
.bindTo(label.rx_text)
.addDisposableTo(disposeBag)
其中,UITextField的rx_text屬性為ControlProperty類型,實現了ControlPropertyType,所以不僅是觀察者類型,還是被觀察者類型,UILabel中的rx_text只是單純的觀察者類型。
bindTo
的另外一個用得比較多的重載方法為:bindTo(variable: RxSwift.Variable<Self.E>) -> Disposable
,這個方法將一個被觀察者與一個Variable(變量)
綁定在一起,這個變量的元素類型跟被觀察者的事件元素類型一致。此方法作用就是把從被觀察者事件流中發射出的事件元素存入變量中,在這里不做演示。
關于bindTo
的其他重載方法在這里就不完全闡述了,剩下的主要是用于對函數的綁定(還有針對柯里化的函數)。
UIBindingObserver
現在介紹的這個東東就跟上面說的被觀察者類型的bindTo
方法密切相關了。
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文件中創建了一個私有的UILabel
擴展,并在擴展中定義了一個只讀計算屬性,屬性的類型為AnyObserver<String>
,為一個事件元素是String
的觀察者類型。當獲取這個屬性值的時候,就返回了與特定UIBindingObserver
關聯的觀察者。
現在我們來看一下UIBindingObserver
的構造方法:
init(UIElement: UIElementType, binding: (UIElementType, Value) -> Void)
方法的第一個參數就是傳入一個要被綁定的視圖的實例,由于現在是在UILabel
的擴展中,所以這里我傳入了self
,代表UILabel
自己;構造方法的第二個參數為一個無返回值的閉包類型,閉包的參數其一就是被綁定了的視圖,其二就是由綁定的被觀察者中所發射出來的事件元素。通過這個閉包,我們能夠將視圖中的某些屬性根據相應的事件元素而進行改變,如例子中label.text = "Hello \(string)"
。當我們執行例子中的binding
函數進行綁定后,TextField
中的字符串每經過修改,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
從名字上可以理解為驅動
(我自己會親切地把它叫做"老司機"),在功能上它類似被觀察者(Observable),而它本身也可以與被觀察者相互轉換(Observable: asDriver, Driver: asObservable),它驅動著一個觀察者,當它的事件流中有事件涌出時,被它驅動著的觀察者就能進行相應的操作。一般我們會將一個Observable
被觀察者轉換成Driver
后再進行驅動操作:
我們沿用上面例子中的UILabel
私有擴展,并修改下binding
方法:
func binding() {
textField.rx_text
.asDriver()
.drive(label.rx_sayHelloObserver)
.addDisposableTo(disposeBag)
}
可見,Driver
的drive
方法與Observable
的方法bindTo
用法非常相似,事實上,它們的作用也是一樣,說白了就是被觀察者與觀察者的綁定。那為什么RxSwift
的作者又搞出Driver
這么個東西來呢?
其實,比較與Observable
,Driver
有以下的特性:
- 它不會發射出錯誤(Error)事件
- 對它的觀察訂閱是發生在主線程(UI線程)的
- 自帶
shareReplayLatestWhileConnected
下面就圍繞著這三個特性一一研究下:
-
當你將一個
Observable
轉換成Driver
時,用到的asDriver
方法有下面幾個重載:asDriver(onErrorJustReturn onErrorJustReturn: Self.E) asDriver(onErrorDriveWith onErrorDriveWith: RxCocoa.Driver<Self.E>) asDriver(onErrorRecover onErrorRecover: (error: ErrorType) -> RxCocoa.Driver<Self.E>)
從這三個重載方法中可看出,當我們要將有可能會發出錯誤事件的Observable
轉換成Driver
時,必須要先將所有可能發出的錯誤事件濾除掉,從而使得Driver
不可能會發射出錯誤的事件。
- 在
Observable
中假如你要進行限流,你要用到方法throttle(dueTime: RxSwift.RxTimeInterval, scheduler: SchedulerType)
,方法的第一個參數是兩個事件之間的間隔時間,第二個參數是一個線程的有關類,如我要在主線程中,我可以傳入MainScheduler.instance
。而在Driver
中我們要限流,調用的是throttle(dueTime: RxSwift.RxTimeInterval)
,只配置事件的間隔時間,而它默認會在主線程中進行。 - 一般我們在對
Observable
進行map
操作后,我們會在后面加上shareReplay(1)
或shareReplayLatestWhileConnected
,以防止以后被觀察者被多次訂閱觀察后,map
中的語句會多次調用:
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
中,框架已經默認幫我們加上了shareReplayLatestWhileConnected
,所以我們也沒必要再加上"replay"相關的語句了。
從這些特性可以看出,Driver
是一個專門針對于UI的特定可觀察者類。并不是說對UI進行相應綁定操作不能使用純粹的Observable
,但是,Driver
已經幫我們省去了好多的操作,讓我們對UI的綁定更加的高效便捷。所以,對UI視圖的綁定操作,我們首選“老司機”Driver
。
DisposeBag
當一個Observable(被觀察者)
被觀察訂閱后,就會產生一個Disposable
實例,通過這個實例,我們就能進行資源的釋放了。
對于RxSwift
中資源的釋放,也就是解除綁定、釋放空間,有兩種方法,分別是顯式釋放以及隱式釋放:
- 顯式釋放 可以讓我們在代碼中直接調用釋放方法進行資源的釋放,如下面的實例:
let dispose = textField.rx_text
.bindTo(label.rx_sayHelloObserver)
dispose.dispose()
這個例子只是為了更明朗地說明顯式釋放方法而已,實際上并不會這樣寫。
-
隱式釋放 則通過
DisposeBag
來進行,它類似于Objective-C ARC
中的自動釋放池
機制,當我們創建了某個實例后,會被添加到所在線程的自動釋放池中,而自動釋放池會在一個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
數據源的解決方案,在GitHub中也有一個開源小庫RxDataSource
,在這里我就不再研究了,有興趣的朋友可以去看看:GitHub RxDataSource。
我這里用一個例子來展示下RxCocoa
中的簡單UITableView
數據源:
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
}()
}
如上,我們能夠將數據封裝在Observable
中,然后在吧Observable
綁定到UITableView
中,通過UITableView
的方法rx_itemsWithCellIdentifier
,我們就能夠進行數據跟Cell的一一對應配置。
到此,UITableView
的數據源就設置好了。UICollectionView
的數據源設置跟UITableView
差不多,在這里就不再作例子了。
項目實戰
下面就是重頭戲了,我將通過折騰出一個小項目來演示RxSwift
的使用,包括基礎以及進階的內容,首先來設定下這個項目:
說簡單點,就是做一個登錄界面(萬能Demo)??,輸入用戶號碼跟密碼,點擊登錄按鈕,即可登錄獲取數據。??
說復雜點,我們要完成下面的要求:
- 用戶號碼輸入框要判斷用戶輸入的是否全是數字,若格式不正確,提示用戶格式錯誤。
- 號碼輸入框輸入的數字最少要有11位,密碼輸入框輸入的字符串長度最少要有6位。
- 要滿足上面的兩條要求,登錄按鈕才可以點擊。
- 登錄按鈕點擊后進行登錄,界面顯示正在轉動的等待視圖,當接收到后臺數據時,等待視圖消失。
- 解析后臺返回的數據,并把數據呈現到界面中。
在這個項目中,我還是使用熟悉的MVVM
架構模式。在開干之前我首先要說幾點:
-
RxSwift
中的ViewModel
是沒有什么明確的狀態的,它的輸出由輸入決定,可以這么說,我們要使用RxSwift
將ViewModel
中的外界輸入(UI觸發、外界事件)轉換成輸出,再由這些輸出去驅動UI界面,并且,ViewModel
做的是轉換,我們不能夠在其中對某個Observable
進行訂閱操作,所以,在ViewModel
中我們是看不到addDisposableTo
的。 - 我對比了一下由
ReactiveCocoa
與RxSwift
實現的ViewModel
,發現使用ReactiveCocoa
實現的ViewModel
中會有比較多的明確狀態變量,比如說現在實現的是登錄的界面,在ReactiveCocoa
的ViewModel
中我們會看到有"userName"、"passWord"等等之類的狀態變量,它是由ReactiveCocoa
將其與UI視圖屬性相綁定的:RAC(self.viewModel, userName) = userNameTextField.rac_textSignal;
,而在RxSwift
實現的ViewModel
,就不會看到這些狀態變量了,有的是驅動外界UI的輸出Driver
,個人認為RxSwift
實現ViewModel
的宗旨是將外界視圖的輸入經過轉變產生輸出,在讓輸出去驅動回UI視圖,所以我在構建ViewModel
類的時候,會在它的構造方法中開設一個接收輸入的參數,其次就在后面的控制器綁定中將ViewModel
的輸出進行訂閱,驅動視圖層。 - 這個項目我使用的第三方庫有
RxSwift
、RxCocoa
、Moya
、Argo
、Curry
,前面兩個在上面有說到;Moya
是一款Swift
語言的網絡請求框架,它是另一款網絡請求框架Alamofire
的再度封裝,它有基于RxSwift
的擴展,能與RxSwift
無縫對接;Argo
是一款小巧的JSON
解析庫,函數柯里化(Currying)
庫Curry
配合著它一起使用,而且,Argo
的解析語法非常新穎奇特,用著感覺非常過癮!
敲代碼走起~
界面
在Storyboard
中布局好登錄界面,分別有用戶電話號碼的輸入框、用戶密碼輸入框、等待視圖(菊花)、提示視圖(用于提醒輸入的錯誤,以及登錄的狀態)、登錄按鈕:
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.