RxSwift進階與實戰

前言

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

ReactiveCocoa's Logo
ReactiveCocoa's Logo

ReactiveCocoa是具有響應式以及函數式編程特點的第三方開源框架,它可以在MVVM架構模式中充當著View(視圖)層與ViewModel(視圖模型)層之間的Binder(綁定者)角色,實現兩個層之間的同步更新。在ReactiveCocoa的世界中,數據與屬性的改變、視圖的操作反饋、方法的調用等都可以被監聽并抽象轉換成事件流,封裝在Signal(信號)中,我們通過對SignalSubscribe(訂閱)就能獲取到其中的事件流,并進行相應的操作。

近期這段時間,我重新折騰起了Swift。在我剛剛初步掌握Swift語言的時候,也就用它做了一個以MVC為架構模式的較為簡單的項目而已,后面寫到一半左右就爛尾了,轉為用Objective-C去折騰另一個較為龐大的項目。在幾天前搞起Swift時,我思考過,有沒有一種解決方案能夠在Swift中像ReactiveCocoa一樣能夠優雅地實現MVVM架構呢?查閱相關資料,我了解到ReactiveCocoa也能在Swift環境下使用,也認識了另一個第三方框架 —— RxSwift,在對其的學習與實踐中,我也越來越中意這貨了。

<font size=5> RxSwift: ReactiveX for Swift</font>
RxSwiftReactiveX(Reactive Extensions)旗下的Swift語言庫,提供了Swift平臺上進行響應式編程的解決方案。Rx的重要角色為Observable(被觀察者)Observer(觀察者)Observable類似于ReactiveCocoa中的Signal,里面裝有事件流,供Observer訂閱。事件流Rx中與ReactiveCocoa一樣具有三類:NextErrorCompleted,代表著繼續事件、錯誤事件、完成事件。我們在使用RxSwift進行iOS開發時,通常會引入另外一個庫:RxCocoa,這個庫將UIKit以及Foundation框架中許多成員,如視圖(View)、控制事件(Control Event)、鍵值觀察(KVO)、通知(Notification)等等進行與RxSwift接入的擴展,將Rx與iOS API無縫連接。

本文主要針對RxSwift闡述它的進階使用,以及在最后結合MVVM項目實戰來鞏固知識點。作為一篇總結我自己對RxSwift學習的文章。
有關RxSwift的基礎教程可前往此項目的GitHub倉庫中下載,里面會有個專門介紹基礎使用的playground工程文件: GitHub: RxSwift

進階講解

bindTo

bindToObservableType協議的幾個重載方法(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)
    }

可見,Driverdrive方法與Observable的方法bindTo用法非常相似,事實上,它們的作用也是一樣,說白了就是被觀察者與觀察者的綁定。那為什么RxSwift的作者又搞出Driver這么個東西來呢?
其實,比較與ObservableDriver有以下的特性:

  • 它不會發射出錯誤(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在此實例上綁定的資源得到釋放。

對于UITableViewCellUICollectionViewCell來說,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)??,輸入用戶號碼跟密碼,點擊登錄按鈕,即可登錄獲取數據。??
說復雜點,我們要完成下面的要求:

  1. 用戶號碼輸入框要判斷用戶輸入的是否全是數字,若格式不正確,提示用戶格式錯誤。
  2. 號碼輸入框輸入的數字最少要有11位,密碼輸入框輸入的字符串長度最少要有6位。
  3. 要滿足上面的兩條要求,登錄按鈕才可以點擊。
  4. 登錄按鈕點擊后進行登錄,界面顯示正在轉動的等待視圖,當接收到后臺數據時,等待視圖消失。
  5. 解析后臺返回的數據,并把數據呈現到界面中。

在這個項目中,我還是使用熟悉的MVVM架構模式。在開干之前我首先要說幾點:

  • RxSwift中的ViewModel是沒有什么明確的狀態的,它的輸出由輸入決定,可以這么說,我們要使用RxSwiftViewModel中的外界輸入(UI觸發、外界事件)轉換成輸出,再由這些輸出去驅動UI界面,并且,ViewModel做的是轉換,我們不能夠在其中對某個Observable進行訂閱操作,所以,在ViewModel中我們是看不到addDisposableTo的。
  • 我對比了一下由ReactiveCocoaRxSwift實現的ViewModel,發現使用ReactiveCocoa實現的ViewModel中會有比較多的明確狀態變量,比如說現在實現的是登錄的界面,在ReactiveCocoaViewModel中我們會看到有"userName"、"passWord"等等之類的狀態變量,它是由ReactiveCocoa將其與UI視圖屬性相綁定的:RAC(self.viewModel, userName) = userNameTextField.rac_textSignal;,而在RxSwift實現的ViewModel,就不會看到這些狀態變量了,有的是驅動外界UI的輸出Driver,個人認為RxSwift實現ViewModel的宗旨是將外界視圖的輸入經過轉變產生輸出,在讓輸出去驅動回UI視圖,所以我在構建ViewModel類的時候,會在它的構造方法中開設一個接收輸入的參數,其次就在后面的控制器綁定中將ViewModel的輸出進行訂閱,驅動視圖層。
  • 這個項目我使用的第三方庫有RxSwiftRxCocoaMoyaArgoCurry,前面兩個在上面有說到;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參數,為的是測試,這樣子系統就不會請求網絡,而是直接通過獲取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.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容