RxSwift進階與實戰(zhàn)

前言

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

ReactiveCocoa's Logo
ReactiveCocoa's Logo

ReactiveCocoa是具有響應式以及函數(shù)式編程特點的第三方開源框架侵佃,它可以在MVVM架構(gòu)模式中充當著View(視圖)層與ViewModel(視圖模型)層之間的Binder(綁定者)角色,實現(xiàn)兩個層之間的同步更新踱蛀。在ReactiveCocoa的世界中劫笙,數(shù)據(jù)與屬性的改變、視圖的操作反饋星岗、方法的調(diào)用等都可以被監(jiān)聽并抽象轉(zhuǎn)換成事件流填大,封裝在Signal(信號)中,我們通過對SignalSubscribe(訂閱)就能獲取到其中的事件流俏橘,并進行相應的操作允华。

近期這段時間,我重新折騰起了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德澈,在對其的學習與實踐中,我也越來越中意這貨了固惯。

<font size=5> RxSwift: ReactiveX for Swift</font>
RxSwiftReactiveX(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

bindToObservableType協(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)
    }

可見,Driverdrive方法與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在此實例上綁定的資源得到釋放。

對于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數(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ù)透绩。??
說復雜點,我們要完成下面的要求:

  1. 用戶號碼輸入框要判斷用戶輸入的是否全是數(shù)字壁熄,若格式不正確帚豪,提示用戶格式錯誤。
  2. 號碼輸入框輸入的數(shù)字最少要有11位草丧,密碼輸入框輸入的字符串長度最少要有6位狸臣。
  3. 要滿足上面的兩條要求,登錄按鈕才可以點擊方仿。
  4. 登錄按鈕點擊后進行登錄固棚,界面顯示正在轉(zhuǎn)動的等待視圖统翩,當接收到后臺數(shù)據(jù)時,等待視圖消失此洲。
  5. 解析后臺返回的數(shù)據(jù)厂汗,并把數(shù)據(jù)呈現(xiàn)到界面中。

在這個項目中呜师,我還是使用熟悉的MVVM架構(gòu)模式娶桦。在開干之前我首先要說幾點:

  • RxSwift中的ViewModel是沒有什么明確的狀態(tài)的,它的輸出由輸入決定汁汗,可以這么說衷畦,我們要使用RxSwiftViewModel中的外界輸入(UI觸發(fā)、外界事件)轉(zhuǎn)換成輸出知牌,再由這些輸出去驅(qū)動UI界面祈争,并且,ViewModel做的是轉(zhuǎn)換角寸,我們不能夠在其中對某個Observable進行訂閱操作菩混,所以,在ViewModel中我們是看不到addDisposableTo的扁藕。
  • 我對比了一下由ReactiveCocoaRxSwift實現(xiàn)的ViewModel沮峡,發(fā)現(xiàn)使用ReactiveCocoa實現(xiàn)的ViewModel中會有比較多的明確狀態(tài)變量,比如說現(xiàn)在實現(xiàn)的是登錄的界面亿柑,在ReactiveCocoaViewModel中我們會看到有"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板熊、RxCocoaMoya察绷、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ò),而是直接通過獲取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相對來說比較難搞笛钝,畢竟我們要處理好每一個輸入輸出的關(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.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末暴构,一起剝皮案震驚了整個濱河市跪呈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌丹壕,老刑警劉巖庆械,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異菌赖,居然都是意外死亡缭乘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來堕绩,“玉大人策幼,你說我怎么就攤上這事∨簦” “怎么了特姐?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長黍氮。 經(jīng)常有香客問我唐含,道長,這世上最難降的妖魔是什么沫浆? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任捷枯,我火速辦了婚禮,結(jié)果婚禮上专执,老公的妹妹穿的比我還像新娘淮捆。我一直安慰自己,他們只是感情好本股,可當我...
    茶點故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布攀痊。 她就那樣靜靜地躺著,像睡著了一般拄显。 火紅的嫁衣襯著肌膚如雪苟径。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天凿叠,我揣著相機與錄音涩笤,去河邊找鬼。 笑死盒件,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的舱禽。 我是一名探鬼主播炒刁,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼誊稚!你這毒婦竟也來了翔始?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤里伯,失蹤者是張志新(化名)和其女友劉穎城瞎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疾瓮,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡脖镀,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了狼电。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蜒灰。...
    茶點故事閱讀 40,503評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡弦蹂,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出强窖,到底是詐尸還是另有隱情凸椿,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布翅溺,位于F島的核電站脑漫,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏咙崎。R本人自食惡果不足惜窿撬,卻給世界環(huán)境...
    茶點故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望叙凡。 院中可真熱鬧劈伴,春花似錦、人聲如沸握爷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽新啼。三九已至追城,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間燥撞,已是汗流浹背座柱。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留物舒,地道東北人色洞。 一個月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像冠胯,于是被迫代替她去往敵國和親火诸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容