使用 RxSwift 進行響應式編程

binggogo2 8年前發布 | 23K 次閱讀 RxSwift Apple Swift開發

您或許曾經聽說過「響應式編程」(Reactive Programming) 一詞,甚至很可能研究過 RxSwift 的相關內容。但是如果您沒有在日常開發中使用響應式編程的話,那么您就真的落后于時代了!在 AltConf 2016 的本次講演中,Scott Gardner 將帶大家走入到響應式編程的世界當中,并告訴大家一個簡單的方法來開始學習 Reactive Swift。他會對 RxSwift 進行一個大致的介紹,同時還會介紹其他的學習資料來幫助您更好的學習 RxSwift。快來看一看使用 RxSwift 進行響應式編程是如何改變您的編碼方式,這將超乎您的想象。

傳統式與響應式大對比

我覺得我應該需要用一個例子,來向各位直觀地展示一下 RxSwift 能夠做些什么。

我們這里要大致說明一下這個例子,這是一個 iOS 應用,我將創建一個 Speaker 的結構體,這只是一個存儲講演者名字、推ter 和頭像的簡單數據結構,此外它還遵循 CustomStringConvertible 協議。

import UIKit

struct Speaker {
    let name: String
    let 推terHandle: String
    var image: UIImage?

    init(name: String, 推terHandle: String) {
        self.name = name
        self.推terHandle = 推terHandle
        image = UIImage(named: name.stringByReplacingOccurrencesOfString(" ", withString: ""))
    }
}

extension Speaker: CustomStringConvertible {
    var description: String {
        return "\(name) \(推terHandle)"
    }
}

接下來,我們要寫一個 ViewModel 。

import Foundation

struct SpeakerListViewModel {
    let data = [
        Speaker(name: "Ben Sandofsky", 推terHandle: "@sandofsky"),
        Speaker(name: "Carla White", 推terHandle: "@carlawhite"),
        Speaker(name: "Jaimee Newberry", 推terHandle: "@jaimeejaimee"),
        Speaker(name: "Natasha Murashev", 推terHandle: "@natashatherobot"),
        Speaker(name: "Robi Ganguly", 推terHandle: "@rganguly"),
        Speaker(name: "Virginia Roberts",  推terHandle: "@askvirginia"),
        Speaker(name: "Scott Gardner", 推terHandle: "@scotteg")
    ]
}

不過我不想讓本次講演變成了關于 MVVM 或者其他諸如此類的架構模式的講演,因此我不會著重介紹這一部分。我在這里所做的就是創建了一個會被 UITableView 所使用的數據源而已,并且我將它提取出來,讓其作為一個能夠創建數據的單獨數據類型。接下來,我們會在持有 UITableView 的視圖控制器當中使用這個類型,因此這只是對代碼做了一點小小的變化而已。接下來在視圖控制器當中,我將要實現標準的 UITableViewDataSource 和 UITableViewDelegate 協議,就如同我們以往所做的那樣。這些代碼完完全全就是樣板代碼 (boilerplate code),我們大家都已經一遍又一遍地寫過這些代碼了。我們最終的效果就是一個 UITableView ,它列出了部分本周在這里進行講演的演講者信息。

class SpeakerListViewController: UIViewController {

    @IBOutlet weak var speakerListTableView: UITableView!

    let speakerListViewModel = SpeakerListViewModel()

    override func viewDidLoad() {
        super.viewDidLoad()
        speakerListTableView.dataSource = self
        speakerListTableView.delegate = self
    }
}

extension SpeakerListViewController: UITableViewDataSource {

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int)-> Int {
        return speakerListViewModel.data.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCellWithIdentifier("SpeakerCell")
            else {
                return UITableViewCell()
        }

        let speaker = speakerListViewModel.data[indexPath.row]
        cell.textLabel?.text = speaker.name
        cell.detailTextLabel?.text = speaker.推terHandle
        cell.imageView?.image = speaker.image
        return cell
    }
}

extension SpeakerListViewController: UITableViewDelegate {
    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        print("You selected \(speakerListViewModel.data[indexPath.row])")
    }
}

好的,現在我將把這個項目 Rx 化,首先第一步我要做的就是修改 ViewModel ,將 data 屬性變成一個可觀察序列對象 (Observable Sqquence)。接下來我會好好解釋一下這個「可觀察序列對象」是什么的,但是現在各位只要知道,這個序列對象當中的內容和我們之前在數組當中所包含的內容是完全一模一樣的。「序列」可以對這些數值進行「訂閱」(Subscribe),就如同 NSNotificaitonCenter 的概念差不多。

import RxSwift

struct SpeakerListViewModel {
    let data = Observable.just([
        Speaker(name: "Ben Sandofsky", 推terHandle: "@sandofsky"),
        Speaker(name: "Carla White", 推terHandle: "@carlawhite"),
        Speaker(name: "Jaimee Newberry", 推terHandle: "@jaimeejaimee"),
        Speaker(name: "Natasha Murashev", 推terHandle: "@natashatherobot"),
        Speaker(name: "Robi Ganguly", 推terHandle: "@rganguly"),
        Speaker(name: "Virginia Roberts",  推terHandle: "@askvirginia"),
        Speaker(name: "Scott Gardner", 推terHandle: "@scotteg")
    ])
}

因此,我們回到視圖控制器當中,我們現在就不用去實現數據源和委托協議了,這里我寫了一些響應式代碼,它們將數據和 UITableView 建立了綁定關系。

import RxSwift
import RxCocoa

class SpeakerListViewController: UIViewController {

    @IBOutlet weak var speakerListTableView: UITableView!

    let speakerListViewModel = SpeakerListViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        speakerListViewModel.data
            .bindTo(speakerListTableView.rx_itemsWithCellIdentifier("SpeakerCell")) { _, speaker, cell in
                cell.textLabel?.text = speaker.name
                cell.detailTextLabel?.text = speaker.推terHandle
                cell.imageView?.image = speaker.image
            }
            .addDisposableTo(disposeBag)

        speakerListTableView.rx_modelSelected(Speaker)
            .subsribeNext { speaker in
                print("You selected \(speaker)")
        }
        .addDisposableTo(disposeBag)
    }
}

我現在會對這段代碼逐行進行解釋。

我添加了一個名為 DisposeBag 的玩意兒,這是 Rx 在視圖控制器或者其持有者將要銷毀的時候,其中一種實現「訂閱處置機制」的方式。這就和 NSNotificationCenter 的 removeObserver 類似。

因此,「訂閱」將在對象銷毀的時候進行處置,它會負責清空它里面的資源。接下來我們使用了 rx_itemsWithCellIdentifier ,這是 Rx 基于 cellForRowAtIndexPath 數據源方法的一個封裝。此外,Rx 也完成了對 numberOfRowsAtIndexPath 方法的實現,這個方法在傳統方式當中是必不可少的,但是這里我們就沒必要實現它了,Rx 已經幫我們完成了。

接下來是這個 rx_modelSelected ,它是 Rx 基于 UITableView 委托回調方法 didSelectRowAtIndexPath 的一個封裝。

好的,這就是全部了。然后來對比一下傳統方式和響應式的異同吧!

實際上,您會發現您已經精簡了將近 40% 的代碼量,而這些所精簡的代碼基本上就是我們之前所說的那些「樣板代碼」。如果我們換個角度比喻的話,這就像我們只需要工作到午餐后,但是卻能夠拿一天的薪水。或者,對于我們這些在美國工作的人來說,就相當于工作到 7 月 4 號,然后剩下的日子就可以去享受帶薪假期了。

是不是感覺很心動呢?

這只不過是一個很簡單的例子而已,但是我們所說的絕對不只是為了精簡代碼而已。我們所要做的,就是要編寫更具有表現力的代碼,尤其是在編寫異步代碼的時候。

那么現在各位可能就會在想了,何為響應式編程?

響應式編程的關鍵在于:將異步可觀察序列對象模型化。這些序列對象可以包含值,就如同我在上一個例子中給大家展示的那樣,但是這些序列對象同時也可以是所謂的「事件流」,比如說單擊或者其他手勢事件。其他所有東西都是建立在這個概念的基礎上的。因此,響應式編程的概念已經存在了 20 年了,但是直到近年它才有了極大的發展,比如說 2009 年所引入的響應式擴展 (Reactive Extension)。

因此,RxSwift 是一系列標準操作符的集合,這些操作符涵蓋了所有的 Rx 實現,它可以被用來創建并與可觀察序列對象協同工作。

當然,我們還有專門面向平臺的第三方庫,比如說我在上個例子為大家展示的 RxCocoa ,它是專門為 iOS、OS X 之類平臺所構建的。RxSwift 是針對 Swift 響應式擴展的官方實現,每個實現方法都實現了在不同語言和不同技術平臺上相同的模式和操作符。

在 Rx 中,基本上所有東西要么是一個可觀察序列對象,要么就是需要和可觀察序列對象進行協同工作的。因此,序列對象將會按需推出其中的內容,這些內容都屬于技術事件。您可以訂閱一個可觀察序列,以便對推出的這些事件作出響應。再強調一遍,這個機制和 NSNotificationCenter 極其類似,但是 Rx 更加優秀。

RxSwift 操作符將執行各種任務,它們是基于事件的。它們通常以異步方式執行,此外 Rx 還是函數式的,因此您或許會采用函數式響應式編程 (Functional Reactive Programming, FRP) 模式,我對這個模式是很滿意的,我覺得這是 RxSwift 應有的樣子。其他的話我就可能不這么看了。

也就是說,Rx 實現了兩種常見的模式:

  1. 首先是觀察者模式 (Observer),它是管理一系列其從屬單元 (Dependents) 的對象,其中包括了觀察者和訂閱者 (Subscriber),一旦發生變化就會發送通知。

  2. 此外是迭代模式 (Iterator),這樣集合或者序列中的值就可以進行遍歷了。

因此,Rx 對于絕大多數現代編程語言來說都是可以實現的。因此,讓我們現在來談論一下某個可觀察序列對象的生命周期吧!

可觀察序列對象的生命周期

這個箭頭表示可觀察序列對象隨時間變化的情況,當某個值或者一系列值被放到序列對象當中的時候,它就會給它的觀察者發送一個「下一步 (Next)」事件,而這個事件當中將會包含這些新增的元素。再次強調一遍,這個過程稱之為發送 (Emitting),而那些值將變成元素 (Element)。不管這里的這些值是什么,比如說觸摸事件、點擊事件、TouchInside 事件還是什么,它們的工作方式都是相同的。(順便提一點,這被稱之為 Marble 圖。)

如果遇到了錯誤,那么序列對象將會發送一個錯誤事件 (Error Event),這其中將包含有錯誤類型實例,這樣您就可以對這個事件做出回應,以便執行錯誤處理,問詢該錯誤,以便查看哪里出了問題。當錯誤發生之后,也就是這條時間線上的 X 表示的地方,它同樣會立刻終止這個序列,當序列中止之后,它就沒辦法再發送更多的事件了,因此一旦您接取到了錯誤事件,就意味著這個序列已經死掉了。

使用 RxSwift 進行響應式編程

序列也可以正常終止,而當序列正常終止之后,它將會發送一個完成事件 (Completed Event),也就是時間線上的豎線表示的地方。

好的,這部分說完了。

也就是說,我在這兒說了很多關于 RxSwift 的內容,不過講道理,當我在談論 RxSwift 的時候,我實際上是指一個更大的功能集,不僅僅包含了 RxSwift 核心庫,還包含了 RxCocoa(這是專門為 iOS、OS X、watchOS 和 tvOS 平臺而專門實現的響應式擴展)。目前為止,RxSwift 社區倉庫當中已經有很多可用的響應式第三方庫了。比如說有 RxDataSources 庫。它借助響應式擴展來為 UITableView 和 UICollectionView 提供了多種更友善的使用方式。

如果您想要跟隨我來一起瀏覽 RxSwift 的話,您可以根據以下說明,去獲取 CocoaPods Playgrounds 插件,這可以創建一個特殊的 Playground,它可以將諸如 RxSwift 之類的第三方庫拉取到其中。

  1. 安裝 ThisCouldBeUsButYouPlaying

    1. bit.ly/podsPlaygrounds

    2. gem install cocoapods-playgrounds

  2. 創建 RxSwiftPlayground

  3. pod playgrounds RxSwift

我首先做的事情就是創建一個封裝我的示例的輔助函數。它會將描述信息打印出來,然后運行示例代碼。

//Please build the scheme 'RxSwiftPlayground' first
import XCPlayground
XCPlaygroundPage.currentPage.needsIndefiniteExecution = true

import RxSwift

func exampleOf(description: String, action: Void -> Void) {
   print("\n--- Example of:", description, "---")
   action()
}

非常簡單,對吧?

我們的第一個例子將要來使用 just 操作符,我們將創建一個包含單個值的可觀察序列,在這個例子中將是一個整數值。接下來,我會訂閱一個事件,這個事件隨后將通過 subscribe 操作符從這個可觀察序列當中傳送出去。每當我接收到事件對象,我都會將其輸出到控制臺上,這里我使用 $0 這個默認參數名。

exampleOf("just") {
   Observable.just(32)
   .subscribe {
        print($0)
   }
}

也就是說,首先我們可以獲取包含該元素的「下一步事件」,接著我們就可以獲取「完成事件」。和錯誤事件類似,一旦某個可觀察序列發送了完成事件,那么這個序列將自動終止。它將無法再次發送任何元素。

exampleOf("just") {
   _ = Observable.just(32)
   .subscribeNext {
     print($0)
   }
}

如果您對 Swift 不熟悉的話,只需要知道, $0 是一個默認參數。只要我想,我就可以像這樣顯式地命名出來,但是如果閉包中只有一兩個參數的話,那么我更傾向于使用諸如 $0 、 $1 之類的默認參數名。這種命名方式更簡單、更方便。

如果您這個時候觀察到 Playground 向您報了一個警告,那是因為訂閱實際上會返回一個表示該訂閱對象的值,這是一個 Disposable 的類型,但是我們現在還沒有對其進行任何處理。

exampleOf("just") {
    Observable.just(32)
    .subscribe { element in
       print(element)
    }
}

首先我將要小小地修改一下這個例子,只需要指明忽略此值即可。接下來要注意到,這個時候我將換用 subscribeNext 操作符。 subscribeNext 只會監聽下一個事件,并且它返回的只是元素,而不是包含元素的事件。因此,我們現在只需要輸出這個元素即可。

exampleOf("just") {
    _ = Observable.just(32)
    .subscribeNext {
        print($0)
    }
}

從多個值中建立可觀察序列

接下來我將要看一看如何從多個值中建立可觀察序列。而這個 of 操作符則可以獲取多個值。我對其進行訂閱,然后向以往一樣將其輸出,只有這時我能夠對這個訂閱對象進行處理 (dispose),而不是指明忽略其返回值,處理可以取消這個訂閱對象。因此,現在我們可以從這個可觀察序列當中輸出每個元素的值了。

exampleOf("of") {
    Observable.of(1, 2, 3, 4, 5)
    .subscribeNext {
        print($0)
    }
    .dispose()
}

我們同樣可以從數組中創建可觀察序列。 toObservable 可以將數組轉換為可觀察序列。

因此首先,我們要創建一個 DisposeBag ,要記住這玩意可以在銷毀的時候處理其中的內容,接下來我會使用 toObserbable 來將一組整數數組轉換為可觀察序列,接下來我會使用 subscribeNext 并將元素值輸出。最后我將這個訂閱對象添加到 DisposeBag 中。

exampleOf(){
    let disposeBag = DisposeBag()
    [1, 2, 3].toObservable()
        .subscribeNext {
            print($0)
        }
        .addDisposableTo(disposeBag)
}

相當簡單。

通常情況下,您會希望創建一個可以添加新元素的可觀察序列,然后讓訂閱對象能夠接收包含這些新添加值的下一步事件,這個操作通常會貫穿整個應用周期,并且以異步方式進行。 BehaviorSubject 正好可以做到這一點。您可以認為 BehaviorSubject 只是簡單地代表了一個隨時間推移而發生更新或更改的值。

我需要向大家解釋一下為什么這種情況不能只維持很短一段時間,但首先,我會在這里創建一個 String 類型的 BehaviorSubject ,初始值為 “Hello”。接下來我會對其進行訂閱,從而讓它能夠接收更新,并將更新值輸出出來。注意到這里我就沒有使用 subscribeNext 了。之后您就知道為什么這么做了。接下來我向這個 BehaviorSubject 中添加一個新的值: “World”。

exampleOf("BehaviorSubject") {
    let disposeBag = DisposeBag()
    let string = BehaviorSubject(value: "Hello")
    string.subscribe {
            print($0)
        }
        .addDisposableTo(disposeBag)
    string.on(.Next("World!"))
}

正如我之前所說, BehaviorSubject 表示了能夠隨時變化的值,但是實際上我并不會對這個值進行修改。因為它仍然是不可修改的。我所做的就是使用 on 操作符來向這個序列當中添加 “World!” 這個詞,里面使用了一個 .Next 枚舉值來封裝要添加的新值。這接下來會使得序列發送一個下一步事件給觀察者。也就是說,這個 on 操作符和 .Next 枚舉值是用來向序列中放入新值的最初級版本,當然我們還有更便利的操作符 onNext ,這也是我更傾向于使用的一個。它所做的事情和前一個例子完全一樣,但是它寫起來更容易,并且閱讀起來也更容易。

其結果是:我看到兩個下一步事件中的元素都被打印出來了,其中包括了初始值的元素,因為在訂閱創建之前,這個相應的值就被賦值了。其原因是 BehaviorSubject 在新的訂閱者訂閱之后,會總是發送最初始或者最新的元素。雖然當 BehaviorSubject 即將被銷毀處理的時候不會自動發送完成事件,但是它同樣還會發送錯誤事件并且終止監聽。我們很有可能不希望使用這個特性。通常情況下,您會希望在訂閱被銷毀的時候,自動實現完成事件的發送,此外還要組織錯誤事件的發生,尤其是我們在談論 UI 繼承這方面事情的時候,這個問題會更為嚴峻。

因此,作為替代,您可以使用名為 Variable 的類型。 Variable 是基于 BehaviorSubject 封裝的。它通過 asObserbable 操作符來暴露其 BehaviorSubject 的可觀察序列。有一些事是 Variable 所做的而 BehiviorSubject 所不能完成的,那就是 Variable 保證不會發生任錯誤事件,并且當它即將被銷毀處理的時候,它就會自動發送一個完成事件。因此,我們使用 Variable 的 value 屬性來獲取當前值,亦或者向序列中設置一個新的值。比起之前的 onNext 來說,我更喜歡這個語法,因此我更傾向于使用 Variable 。至于其他原因再次就暫時不表了。

exampleOf("Variable") {
    let disposeBag = DisposeBag()
    let number = Variable(1)
    number.asObservable()
        .subscribe {
            print($0) 
        }
        .addDisposableTo(disposeBag)
    number.value = 12
    number.value = 1_234_567
}

這些就是 RxSwift 中響應式編程的基礎構建模塊了,除此之外還有其他更多的類型,當然還有更多操作符。我隨后可能會涉及其中的一些。但是一旦您了解了其中的基礎內容,其他的一切都是小問題了。我覺得理解剩下的內容不會太難。再次強調一下,您只需要記住一點:無事不序列。我們不應該對值本身進行更改。您應該向序列中添加新值,接下來觀察者便可以響應這些值,因為這些值已經由可觀察序列發送給了觀察者。

現在我已經向各位介紹了幾種創建可觀察序列的方法,現在我將講述幾種變換可觀察序列的方法。

所以大家都對 Swift 的 map 方法有所了解了,對嗎?

您可以創建一個數組,然后你可以調用數組的 map 方法,然后使用一個閉包以向其提供執行相關變換的操作,隨后,這會返回一個數組,這個數組中包含了所有已經完成變換的元素。Rx 擁有自己的 map 操作符,這和傳統的 map 操作符是非常相似的。

在這里,我會使用 of 操作符來創建一個包含整數的可觀察序列。接著,我使用 map 操作符來將這個序列進行變換,使得其中的每個元素都乘以它們自身,隨后我使用 subscribeNext 來輸出每個元素的值。

exampleOf("map") {
    let disposeBag = DisposeBag()
    Observable.of(1, 2, 3)
        .map { $0 * $0 }
        .subscribeNext { print($0) }
        .addDisposableTo(disposeBag)
}

這就是 map 的用法了。

在 Marble 圖中,它是這么表示的:

Map 會變換每一個從源可觀察序列 (Source Observable Sequence) 發射出去的元素。那么,如果一個可觀察序列當中包含有您想要進行監聽的可觀察序列屬性的話,并且你還需要訂閱這個序列的話,這個時候會發生什么呢?注意我們的關鍵點:可觀察序列當中包含了可觀察序列屬性。

這時,我們只需更換下思路,你會發現前方豁然開朗。RxSwift 的 flatMap 可以解決這一點,它與 Swift 標準的 flatMap 方法及其相似,不過不同的是,RxSwift 的 flatMap 只能用于可觀察序列集,并且它是以異步的方式進行的,它會將包含包含可觀察序列集的一個可觀察序列重組為一個單獨的序列。 flatMap 同樣也會對從可觀察序列當中發送出來的元素執行相應的變換操作。

原文 “Down the rabbit hole”,語出愛麗絲夢游仙境,表示「進入了仙境、進入了另一片天地」,就如同「桃花源記」的「世外桃源」。

因此,我在這里要一步一步地來講解這個例子:

首先,我們這里創建了一個 Person 接哦古提,它包含了一個類型為 Variable<String> 的 name 變量,因此, name 當中包含了一個可觀察序列,但是由于我將其聲明成了一個變量,因此我們不僅僅能夠向這個序列上增加值,而且這個序列本身還可以重新賦值。

exampleOf("flatMap") {
     let disposeBag = DisposeBag()
    struct Person {
       var name: Variable<String>
    }
    let scott = Person(name: Variable("Scott"))
    let lori = Person(name: Variable("Lori"))
    let person = Variable(scott)
    person.asObservable()
       .flatMap {
         $0.name.asObservable()
       }
       .subscribeNext {
         print($0)
       }
       .addDisposableTo(disposeBag)
     person.value = lori
     scott.name.value = "Eric"
   }

這只是一個虛構的里子,只是為了向您展示這里面的工作原理。

這里我創建了幾個 Person 的實例,Scott 和 Lori。接下來,我會將 Person 可觀察化 (as observable) 后,然后使用 flatMap 來訪問其 name 可觀察序列,之后將新的值輸出,

記住對于 Variable 來說,我們需要使用 asObservable 來訪問其底層的可觀察序列。因此,Person 當中的內容是 Scott 的,因此我們首先會得到輸出 Scott 。但是之后我直接將 Person 的值重新賦值成了 Lori,因此之后我們會得到輸出 Lori 。最后,我們向 Scott 的序列當中放入了一個新的值 Eric 。

因為我使用了 flatMap ,因此這兩個序列實際上都仍處于活躍狀態,因此這時候我們會看到 Eric 打印了出來。這是一個很棘手的問題。這往往會導致內存泄漏等問題發生,因此除非你覺得你必須要使用 flatMap ,否則的話請使用別的東西來替代它。所用的替代物也就是 flatMapLatest 。因此如果我在這里轉而使用 flatMapLatest ,而不是使用 flatMap ,它的功能和 flatMap 基本一模一樣,但是接下來它會將當前訂閱切換為最后一個序列,因此它會忽略來自之前序列的發送值 (emission)。

exampleOf("flatMapLatest") {
    let disposeBag = DisposeBag()
    struct Person {
        var name: Variable<String>
    }
    let scott = Person(name: Variable("Scott"))
    let lori = Person(name: Variable("Lori"))
    let person = Variable(scott)
    person.asObservable()
        .flatMapLatest {
            $0.name.asObservable()
        }
       .subscribeNext {
         print($0)
        }
        .addDisposableTo(disposeBag)
    person.value = lori
    scott.name.value = "Eric"
}

也就是說,一旦我將 person.value 設為 Lori 之后,Scott 的發送值將會被忽略。

當我對 scott.name 進行設置的時候,Eric 將不再輸出。因此,講道理,我們有這個看起來像是同步操作的示例來向您展示了 flatMapLatest 的內部工作原理,但是這個操作符在諸如網絡操作之類的異步使用情形當中使用更為頻繁,之后我將會向大家簡要地舉一個例子說明一下。

RxSwift 同樣也引入了一系列非常好用的調試操作符。其中之一名為 debug 。你可以想下面這樣,向一個鏈式步驟當中添加 debug 。

exampleOf("flatMapLatest") {
    let disposeBag = DisposeBag()
    struct Person {
        var name: Variable<String>
    }
    let scott = Person(name: Variable("Scott"))
    let lori = Person(name: Variable("Lori"))
    let person = Variable(scott)
    person.asObservable()
        .debug("person")
        .flatMapLatest {
            $0.name.asObservable()
        }
       .subscribeNext {
         print($0)
        }
        .addDisposableTo(disposeBag)
    person.value = lori
    scott.name.value = "Eric"
}

你可以選擇是否向其中添加描述字符串,隨后它會輸出所接收到的每一個事件的詳細信息。如果你初學 RxSwift,并且需要找出所有這些部件是如何工作的話,那么這個命令非常有用。

在 RxSwift 中還有很多的操作符。我在話題繼續之前,我想要簡要地介紹其中的一些,我會盡可能地給大家展示,之后我會給大家分享一些有用的資源,這樣大家在回去之后,如果感興趣的話可以再深入了解一下。

下面這個示例是關于如何使用 distinctUntilChanged ,這個操作符用來消除連續重復 (suppress consecutive duplicate),也就是說如果某個值和上一個值相同,那么在新的序列當中,這個值將會被忽略。 searchString 初始只包含了值 iOS ,之后我會使用 map 方法將字符串變換為小寫字符串,之后再使用 distinctUntilChanged 來進行過濾,如果新增加的值和上一個值相同的話,那么這個新增的值將被忽略。之后我們再對其進行訂閱,輸出其中的結果。

exampleOf("distinctUntilChanged") {
    let disposeBag = DisposeBag()
    let searchString = Variable("iOS")
    searchString.asObservable()
        .map { $0.lowercaseString }
        .distinctUntilChanged()
        .subscribeNext { print($0) }
        .addDisposableTo(disposeBag)
    searchString.value = "IOS"
    searchString.value = "Rx"
    searchString.value = "ios"
}

這個序列的初始值是 iOS,因此首先這個初始值會先被打印出來,之后,我們向序列中添加了同樣的值。你會發現,即使大小寫不同,但是只要其字母相同,由于我們使用了 distinctUntilChanged ,因此這個值仍然會被忽略掉。因此,之后我們向序列當中添加了新值 Rx,這個時候能夠成功輸出出來,之后我們又重新向序列添加 ios 這個值,而這個時候,ios 就被成功輸出了,因為上一個值是 rx,而這倆并不相同。

有些時候,您可能想要同時觀察多個序列。這個時候我們就可以使用這個 combineLatest 操作符了,它會將多個序列聯合成一個單獨的序列。您或許會想到,我們可以使用我幾分鐘之前向大家介紹的 flatMapLatest 操作符啊。事實上, flatMapLatest 是 map 和 combineLatest 兩個操作符的聯合,哦對了還有 switchLatest 操作符。

這里是另一個 Rx 項目類型,名為: PublishSubject ,它同 BehaviourSubject 非常相似,只是 PublishSubject 不需要初始值,并且它也不會將最后一個值重播給新的訂閱者。這就是 PublishSubject 和 BehaviorSubject 的區別所在。

接下來我會使用 combineLatest ,它會等待在生成任何下一步事件之前,等待每一個源可觀察序列完成元素的發送。但是一旦所有的源可觀察序列都發送下一步事件之后,每當這些源可觀察序列發送了新值, PublishSubject 都會立即發送下一步事件。因為我們只是剛剛完成了一輪循環,我們還沒有將這些值放到數字序列上面來的,這個時候字符串序列當中也沒有值。因此,這個時候訂閱操作不會產生任何反應。

這個時候,只有 “Nothing yet” 這個信息會被輸出出來。一旦我向字符串序列當中添加了新的值,這個時候字符串序列和數字序列當中的最新值就會被輸出出來。隨著值的新增,每當源序列發送了新的元素,這兩個序列當中的最新元素都將會被輸出出來。

exampleOf("combineLatest") {
    let disposeBag = DisposeBag()
    let number = PublishSubject<Int>()
    let string = PublishSubject<String>()
    Observable.combineLatest(number, string) { "\($0) \($1)" }
        .subscribeNext { print($0) }
        .addDisposableTo(disposeBag)
    number.onNext(1)
    print("Nothing yet")
    string.onNext("A")
    number.onNext(2)
    string.onNext("B")
    string.onNext("C")
}

下面是關于 takeWhile 的示例,只要滿足了特定的條件,才會發送相應的元素。

首先,我將一個整數數組變為可觀察序列,接下來我使用 takeWhile 來附加條件,只接收小于 5 的元素。一旦新的元素大于等于 5,那么序列將會終止并結束,然后發送一個完成消息。

exampleOf("takeWhile") {
    [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1].toObservable()
        .takeWhile { $0 < 5 }
        .subscribeNext { print($0) }
        .dispose()
}

RxSwift 同樣也有 reduce 操作符,它和 Swift 當中的 reduce 方法一樣,不過有些時候您可能想要對 reduce 過程當中的過程值進行操作,為此,你可以使用 RxSwift 的 scan 操作符。

exampleOf("scan") {
    Observable.of(1, 2, 3, 4, 5)
        .scan(10, accumulator: +)
        .subscribeNext { print($0) }
        .dispose()
}

因此, scan 操作符將會累積一序列值,從初始值開始,然后返回每一個過程值 (Intermediate Value)。和 reduce 操作符類似, scan 可以接收一個標準數學運算符,比如說 + 等等,或者您也可以傳遞一個相同函數類型的閉包進去。

exampleOf("scan") {
    Observable.of(1, 2, 3, 4, 5)
        .scan(10) { $0 + $1 }
        .subscribeNext { print($0) }
        .dispose()
}

還記得嗎,某些可觀察序列可以發送錯誤事件,因此我舉一個非常簡單,但卻是真實的示例來演示你該如何訂閱并處理錯誤事件。

我會創建一個可觀察序列,它會被一個錯誤給立即終止。控制臺所給出的錯誤信息并不是很有用。但是我接下來可以使用 subscribeError 操作符,這樣我就可以處理錯誤,在這個例子中我只是簡單的打印出來。

exampleOf("error") {
    enum Error: ErrorType { case A }
    Observable<Int>.error(Error.A)
        .subscribeError {
            // Handle error
            print($0) 
        }
        .dispose()
}

好的,那么現在讓我給大家展示一個使用 Rx 擴展的網絡示例。這是一個帶有 UITableView 的基礎簡單視圖應用,我們還創建了一個 Repository 模型,它是一個含有 name 和 url 字符串類型屬性的結構體。

struct Repository {
    let name: String
    let url: String
}

然后就是視圖控制器當中,我創建了一個搜索控制器,然后對其進行了相關的配置,然后在 UITableView 的頭部添加了一個搜索欄。

import RxSwift
import RxCocoa

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!

    let searchController = UISearchController(searchResultsControler: nil)
    var searchBar: UISearchBar { return searchController.searchBar }

    var viewModel = ViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        configureSearrchController()

        searchBar.rx_text
            .bindTo(viewModel.searchText)
            .addDisposableTo(disposeBag)

        searchBar.rx_cancelButtonClicked
            .map{ "" }
            .bindTo(viewModel.searchText)
            .addDisposableTo(sidposeBag)

        viewModel.data
            .drive(tableView.rx_itemsWithCellIdentifier("Cell")) { _, repository, cell in
                cell.textLabel?.text = repository.name
                cell.detailTextLabel?.text = reopsitory.url
            }
            .addDisposableTo(disposeBag)
    }

    func configureSearchController() {
        searchController.obscureBackgroundDuringPresentation = false
        searchBar.showsCancelButton = true
        searchBar.text = "scotteg"
        searchBar.placeholder = "Enter GitHub Id, e.g., \"scotteg\""
        tableView.tableHeaderView = searchController.searchBar
        definePresentationContext = true

    }
}

我將搜索欄的 rx_text 進行了綁定,所謂的 rx_text ,是 Rx 基于 UISearchBar text 屬性的一個封裝,我們在這里直接將其綁定到 ViewModel 的 searchText 這個可觀察序列當中。

我同樣也將搜索欄的 rx_cancelButtonTapped 綁定到了 ViewModel 的 searchText 序列上了。此外,我使用 map ,以便在取消按鈕被按下時,向序列中放入一個空字符串。 rx_cancelButtonTapped 是 Rx 基于 UISearchBarDelegate searchBarCancelButtonClicked 回調的一個封裝。

最后,我將 ViewModel 的 data 序列綁定到 UITableView ,就如同我在第一個示例當中所做的那樣。因此,這就是這個視圖控制器的全部工作了。現在讓我們來看看 ViewModel 當中的內容。在真實環境當中,我可能會將這段網絡代碼劃分到一個單獨的 API 管理器當中,但是我覺得在這里,我們只用一個屏幕就能全部展示全部內容的話,看起來會更容易一些。

這里我們有一個 Variable 類型的 searchText 屬性,要記住,這里面封裝了一個可觀察序列。同樣的,這里面也有一個可觀察序列 data 。

這里我使用了另一個名為 Driver (老司機,大誤)的數據類型,這里同樣也封裝了一個可觀察序列,但是它提供了額外的好處,尤其是當我們在對 UI 進行綁定的時候,這時候就能體現出它的優勢所在了。老司機永不會犯錯,并且老司機永遠都只會在主線程上飆車。 throttle 操作符可以延遲網絡請求的訪問,實際上它會過濾發送過快的網絡請求,比如說如果你正在使用類似這樣的預輸入搜索類型應用的話,它可以防止每次在鍵入新字符的時候都會觸發 API 調用這類情況的發生。在本例中,我將過濾限時設置為 0.3 秒。接著我再次使用了 distinctUntilChanged ,呃,這貨我之前就向大家演示過了,如果新的輸入和上一個輸入值相同的話,那么這個新的輸入值就會被忽略。接著我還使用了 flatMapLatest ,它會直接切換到最新的這個可觀察序列。這是因為,當你在搜索欄當中鍵入了額外的文本之后,你就需要接收新的搜索結果了,因為你不會再關心之前的結果序列。 flatMapLatest 會自行為你完成這項工作,因此它會切換到最新的可觀察序列,并且忽略之前序列所發送的東西。

接著,我在 flatMapLatest 的作用域當中調用了 getRepositories 方法,它接收搜索文本作為參數,然后返回一個可觀察數組,這個數組中存放了一系列 Repository 對象,這些對象我在視圖控制器當中就已經和 UITableView 建立綁定關系了。

import RxSwift
import RxCocoa

struct ViewModel {

    let searchText = Variable("")
    let disposeBag = DisposeBag()

    lazy var data: Driver<[Repository]> = {
        return self.searchText.asObservable()
            .throttle(0.3, scheduler: MainScheduler.instance)
            .distinctUntilChanged()
            .flatMapLatest {
                    self.getRepositories($0)
            }
            .asDriver(onErrorJustReturn: [])
    }()

    func getRepositories(githubId:String) -> Observable<[Repository]> {
        guard let url = NSURL(string: "https://api.github.com/users/\(gitHubId)/repos")
            else { return Observable.just([]) }
        return NSURLSession.sharedSession()
            .rx_JSON(NSURLRequest(URL: url))
            .retry(3)
            .map {
                var data = [Repository]()

                if let items = $0 as? [[String: AnyObject]] {
                    items.forEach {
                        guard let name = $0["name"] as? String,
                            url = $0["url"] as? String
                            else { return }

                        data.append(Repository(name: name, url: url))
                    }
                }

                return data
        }
    }
}

這段網絡訪問代碼做了很多事情,讓我來逐行講解吧。

首先我創建了一個 URL,我會在 NSURLRequest 當中使用它。接著我使用 NSURLSession 的 sharedSession 單例,然后使用它的 rx_JSON 擴展。 rx_JSON 接收一個 NSURLRequest 作為參數,它會返回一個 JSON 數據已解析完畢的可觀察序列。并且網絡這種東西大家都懂的,很容易出錯,所以我會使用這個 retry 操作符,它允許我在遇到網絡問題的時候,嘗試重新請求這個網絡請求,3 次之后如果還不行,就放棄訪問。

最后,我使用 map 操作符來創建一個 Repository 類型的數組,我將得到的 JSON 值轉換成 Repository 實例,然后將這個數組返回。好吧,解釋這段代碼比我寫這段代碼還要累。

因此,我們所用的代碼只用了這么幾行,就完成了我們想要的功能,實現了一個很好用的 Github repo 應用,我覺得這個示例用來演示網絡訪問之類的操作是再好不過的了。

好的,我在這里已經喋喋不休地講了很多術語、基礎操作符了,還講了幾個示例,也就是如何在 UITableView 和網絡訪問中使用 RxSwift。我還想再提一個內容。我想要大概介紹一個新的輔助庫,它為 Core Data 提供了 Rx 擴展。

前不久,RxCoreData 推了出來,但是它仍處于雛形階段,還很不完善,我無意于批評任何人,其實該接受批評的是我自己,因為是我把它放出來的。所以我仍需要繼續完善它。如果諸位對此感興趣的話,歡迎來提 PR。

好的,現在我們來看一下這個基本的視圖控制器實現方式,我已經完成了 Core Data、 UITableView 和 RxSwift 的基礎配置。

首先第一件事是這里,我使用了 rx_tap 擴展,它是基于 TouchUpInside 事件的 Rx 封裝,它本質上是用來替代 IBAction 動作塊。接下來,我使用 map 來創建一個新的事件實例,每次這個按鈕被按下的時候,都會觸發這個操作。 Event 是一個簡單的結構體,它里面包含了 ID 和日期。接下來我會對其進行訂閱,然后使用 NSManageObject 的這個 update 擴展,如果對象存在的話,它會對這個對象進行更新,如果對象不存在的話,它會插入這個對象。最后,我使用 rx_entities 擴展,它可以接受一個提取請求 (fetch request),在本例中,它還可以接受一個 Persistable 類型,然后設置其排序描述符 (sort descriptor),這樣它就可以自行為你創建一個提取請求,最后它會返回該 Persistable 類型的一個可觀察數組。

接著我使用相同的 rx_itemsWithCellIdentifier 擴展來將其綁定到 UITableView 上面,這是我第二次使用它了。

import RxSwift
import RxCocoa
import RxDataSources
import RxCodeData

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var addBarButtonItem: UIBarButtonItem!

    var managedObjectContext: NSManagedObjectContext!
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        bindUI()
        configureTablView()
    }

    func nidUI() {
        addBarButtonItem.rx_tap
            .map { _ in
                Event(id: NSUUID().UUIDString, date: NSDate())
            }
            .subscribeNext { [unowned self] event in
                _ = try? self.managedObjectContext.update(event)
            }
            .addDisposableTo(disposeBag)
    }

    func configureTableView() {
        tableView.editing = true

        managedObjectContext.rx_entities(Event.self, sortDescriptors: [NSSortDescriptor(key: "date", adscending: false)])
            .bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {
                cell.textLabel?.text = "\(event.date)"
            }
            .addDisposableTo(disposeBag)
    }
}

因此,這就是我們如何用簡短的幾行代碼就實現了一個可提取檢索結果的控制器——所謂的驅動化的 UITableView 。對于刪除操作來說,我可以使用這個 rx_itemDeleted 擴展,它會返回要刪除項目的索引路徑,然后我使用 map 和 rx_modelAtIndexPath 來取得這個對象,然后在訂閱列表當中將其刪除掉。

//Copyright ? 2016 Scott Gardner. All rights reserved.

func configureTableView() {
    tableView.editing = true

    managedObjectContext.rx_entities(Event.self, sortDescriptors: [NSSortDescriptor(key: "date", adscending: false)])
        .bindTo(tableView.rx_itemsWithCellIdentifier("Cell")) {
            cell.textLabel?.text = "\(event.date)"
        }
        .addDisposableTo(disposeBag)
    self.tableView.rx_itemDeleted
        .map { [unowned self] indexPath in
            try self.tableView.tx_modelAtIndexPath(indexPath) as Event
        }
        .subscribeNext { [unowned self] deletedEvent in
            _ = try? self.managedObjectContext.delete(dletedEvent)
        }
        .addDisposableTo(disposeBag)
}

好的,這就是我要講的全部內容了。

那么,我有沒有引起各位對 RxSwift 的興趣了呢?即便您此前按從未接觸過 RxSwift。好的,看來我還是很成功的。這正是我所期望的局面。這個時候,您可能會有些疑惑,我該怎么入門 RxSwift 呢?

事實上, 您已經入門了

這是因為在本次講演中,我已經帶大家過了很多非常基礎的概念了,這些概念都是您入門所必須要掌握的,這使得大家已經邁出了第一步。此外,我還給大家展示了一些真實示例,這可能已經激起了大家的興趣,大家可能已經躍躍欲試,想要用 RxSwift 做點東西了。

因此,實際上大家下一步需要做的,就是去熟悉 RxSwift 那些常用的操作符。您完全不必去學習所有的操作符。因為在日常工作當中,只有一些是非常有用的,至于其他的操作符,如果有必要的話,完全可以隨時去查閱文檔。RxSwift 的美妙在于,盡管 Swift 會不停地演變、不停地變化,但是 RxSwift 操作符仍然會保持相對穩定。

這些操作符已經在不同的平臺當中存在了很長一段時間了,它們在不同的平臺上所實現的功能都是一致的,因此隨著時間的推移,這些操作符的功能仍然是相對一致的。盡管它內部的實現細節很可能會發生變化,但是你仍然是以同樣的方式來使用它們。也就是說,只需要學習一次這些操作符,你就可以編寫更為簡潔的代碼了。

在 RxSwift 項目倉庫當中,有一個可以互動的 Rx Playground ,它將一步一步的引導你學習絕大多數的操作符,我會在最后為大家提供它的鏈接。這個 Playground 同樣也是依照類型來分類的,因此這使得它的參考價值是非常大的。因此如果你準備學習 iOS、macOS、watchOS 以及 tvOS 平臺所特有的響應式擴展的話,最好的方法就是學習這個 Rx 示例應用,它同樣也是 RxSwift 倉庫的一部分。在這個倉庫當中,還存放著其他很有用的示例,此外還有同時面向 iOS 和 macOS 的示例,大家可以盡情探索。

我同樣還創建了一個 RxSwiftPlayer 項目,它和我們前一陣子所創建的、用于探索 Core Animation API 的播放器項目非常相似。這個項目實際上還未完工,我計劃盡快將其完成。最后,我仍然也會提供相應的鏈接的。

總而言之,一旦大家掌握了這些東西,那么 Rx 世界對你來說就盡在手中的。在 Rx 社區當中,針對 Apple 平臺的開源 Rx 項目和 Rx 第三方庫的數目已經越來越多了。

See the discussion on Hacker News .

 

來自:https://realm.io/cn/news/altconf-scott-gardner-reactive-programming-with-rxswift/

 

 本文由用戶 binggogo2 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!