RxSwift_v1.0筆記——23 MVVM with RxSwift
RxSwift是一個(gè)很大的話題,本書之前沒有覆蓋任何應(yīng)用構(gòu)架的細(xì)節(jié)。是因?yàn)镽xSwift不會(huì)強(qiáng)迫在你的應(yīng)用上使用任何特定的構(gòu)架。不過,因?yàn)镽xSwift與MVVM一起工作更合適,本章將專注于討論討論特殊的構(gòu)架樣式。
Introducing MVVM 353
MVVM代表了Model-View-ViewModel;它與Apple的親兒子MVC有略微不同的實(shí)現(xiàn)。
用一個(gè)開放的思想來(lái)處理MVVM是很重要的。MVVM不是軟件構(gòu)架的萬(wàn)能藥;當(dāng)然,考慮到MVVM是一個(gè)軟件范式,使用它是朝著好的應(yīng)用構(gòu)架邁出的第一步,尤其是你開始是MVC的思維方式。
Some background on MVC 354
現(xiàn)在你對(duì)MVVM和MVC可能感覺有一點(diǎn)矛盾(tension)。它們之間有什么聯(lián)系?它們非常相似,甚至你可以認(rèn)為它們是遠(yuǎn)房親戚。但是解釋它們之間的不同點(diǎn)任然是必要的。
在本書(和其他關(guān)于編程的書)中的大部分的例子使用MVC樣式來(lái)寫代碼示例。MVC是對(duì)許多簡(jiǎn)單的app來(lái)說是簡(jiǎn)單的樣式,它看起來(lái)像這樣:

每個(gè)類分配一個(gè)類別:controller類扮演中間角色讓model和view能夠更新,views僅僅在屏幕上顯示數(shù)據(jù)并發(fā)送事件(如手勢(shì))到controller。最后models讀和寫數(shù)據(jù)來(lái)固化app狀態(tài)。
MVC是簡(jiǎn)單的樣式,它能暫時(shí)(for a while)為你服務(wù),但是當(dāng)你的app成長(zhǎng)后,你將注意到許多類既不是view又不是model,所以只能是controllers。你開始調(diào)入一個(gè)普遍的陷阱,在一個(gè)controller類中增加了越來(lái)越多的代碼。由于你是從iOS應(yīng)用程序啟動(dòng)視圖控制器,最簡(jiǎn)單的方法是將所有代碼放入該視圖控制器類。因此,MVC代表“Massive View Controller”的老笑話,因?yàn)榭刂破骺梢猿砷L(zhǎng)為數(shù)百甚至數(shù)千條行。
過載你的類是一個(gè)不好的做法,但不一定是MVC模式的缺點(diǎn)。例如:蘋果的許多開發(fā)人員都是MVC的粉絲,他們生產(chǎn)(turn out)了非常好的macOS和iOS軟件。
Note:你可以閱讀更多關(guān)于MVC在蘋果專用的文檔頁(yè)面:https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
MVVM to the rescue 354
MVVM看起來(lái)很像MVC,但一定感覺更好。喜歡MVC的人通常也喜歡MVVM,這個(gè)新的樣式讓他們更容易解決許多MVC普遍的問題。
與MVC明顯不同的是一個(gè)叫ViewModel的新種類:

ViewModel在構(gòu)架中作為一個(gè)核心角色:它負(fù)責(zé)業(yè)務(wù)邏輯并與模型和視圖進(jìn)行對(duì)話。
MVVM有以下簡(jiǎn)單的規(guī)則:
- Models不直接與其他類對(duì)話,但是他們能發(fā)射關(guān)于數(shù)據(jù)變化的通知。
- View Models與Models對(duì)話并暴露數(shù)據(jù)給View Controllers。
- View Controllers僅僅與View Models和Views會(huì)話,他們處理視圖的生命周期并綁定數(shù)據(jù)到UI組件。
- Views僅僅通知事件給視圖控制器(就像MVC一樣)。
等等,View Model不正是做了MVC中控制器做的事嗎?是,也不是。
正如先前所說的,普遍的問題是視圖控制器塞入了不是控制視圖的代碼。MVVM通過把視圖控制器與視圖組合來(lái)嘗試解決這個(gè)問題,并讓它只負(fù)責(zé)控制視圖。
MVVM構(gòu)架的另一個(gè)好處是增加代碼的可測(cè)試性。把視圖的生命周期從業(yè)務(wù)邏輯分離,讓測(cè)試視圖控制器和視圖模型變的非常簡(jiǎn)單。
最后但并非最不重要的是,視圖模型完全從顯示層分離,當(dāng)需要的時(shí)候,能夠在不同平臺(tái)間重用。你可以僅僅替換視圖/視圖控制器對(duì),然后遷移你的app從iOS到macOS,甚至是tvOS。
What goes where? 355
但是,不要以為一切都應(yīng)該在你的View Model類中。
這與你最終在MVC一樣,將是同樣愚蠢的。您可以基于你的代碼來(lái)明確劃分和分配責(zé)任。因此,留著View Model作為數(shù)據(jù)和屏幕之間的大腦,但是確保你分離了網(wǎng)絡(luò),導(dǎo)航,緩存和相似的職責(zé)到其他類。
如果它們不屬于任何MVVM類別,這些額外的類如何處理呢?MVVM對(duì)于這些沒有硬性規(guī)定,但是在本章你將工作的項(xiàng)目,它將給你介紹一些可行的解決方案。
本章將介紹一個(gè)好方法,它將通過其初始化或盡可能在其生命周期之后,注入View Model所需的所有對(duì)象。這就是說你能夠?qū)㈤L(zhǎng)時(shí)間活動(dòng)的對(duì)象,像是API類的狀態(tài)或來(lái)自視圖模型的固化層對(duì)象到另一個(gè)視圖模型:

在本章的項(xiàng)目“Tweetie”中,您將以這種方式傳遞一些東西,例如關(guān)于應(yīng)用內(nèi)導(dǎo)航(Navigator)的對(duì)象,當(dāng)前登錄的Twitter帳戶(TwitterAPI.AccountStatus)等等。
但是MVVM的唯一好處是讓代碼變的更短嗎?如果使用得當(dāng),MVVM比MVC有更多優(yōu)勢(shì):
- 視圖控制器趨于更簡(jiǎn)單和名副其實(shí),因?yàn)樗奈ㄒ回?zé)任是控制視圖。MVVM更易于使用RxSwift/RxCocoa,因?yàn)槟軌蚪壎╫bservables到UI組件是MVVM的關(guān)鍵能力。
-
視圖模型有清晰的Input -> Output樣式,并且在為預(yù)期輸出提供預(yù)定義的輸入和測(cè)試時(shí)非常容易測(cè)試。
-
通過創(chuàng)建模擬視圖模型并對(duì)預(yù)期的視圖控制器狀態(tài)進(jìn)行測(cè)試,形象化地測(cè)試視圖控制器變得更加容易。
最后但并非最不重要的一點(diǎn),因?yàn)镸VVM是一個(gè)偉大的分離至MVC,它也可以作為一個(gè)啟發(fā)和靈感來(lái)探索更多的軟件架構(gòu)模式。
想試試MVVM? 在您閱讀本章時(shí),您將看到其許多好處。
Getting started with Tweetie 357
本章,你將工作在叫做Tweetie的多平臺(tái)項(xiàng)目。它是一個(gè)非常簡(jiǎn)單的Twitter-powered應(yīng)用,它使用一個(gè)預(yù)定義的用戶列表來(lái)向用戶顯示推文。默認(rèn)情況下,起始程序項(xiàng)目使用的是具有(featuring)本書所有作者和編輯者的Twitter列表。如果你喜歡,你可以很容易的改變這個(gè)列表來(lái)轉(zhuǎn)換項(xiàng)目為運(yùn)動(dòng),寫作,攝影app。
這個(gè)項(xiàng)目面向macOS和iOS,并通過使用MVVM樣式來(lái)解決許多現(xiàn)實(shí)生活(real-life)中的編程任務(wù)。有許多代碼包含在了起始項(xiàng)目中,你將聚焦在MVVM相關(guān)部分。
當(dāng)你完成本章后,你將見證MVVM有助于區(qū)分以下內(nèi)容:
- 與UI相關(guān)的代碼是特定于平臺(tái)的,例如為iOS的視圖控制器使用UIKit ,以及分離的macOS獨(dú)有的視圖控制器使用Cocoa。
- 代碼按原樣重用,因?yàn)樗灰蕾囉谔囟ㄆ脚_(tái)的UI框架,例如模型和視圖模型中的所有代碼。
是時(shí)候潛入了!
Project structure 357
安裝所有CocoaPods,打開項(xiàng)目,預(yù)覽下項(xiàng)目結(jié)構(gòu)。
在項(xiàng)目導(dǎo)航中,你將發(fā)現(xiàn)有很多文件夾:

- Common Classes:在macOS與iOS間共享代碼。包含一個(gè)RxReachability類擴(kuò)展,在 UITableView, NSTableView上的擴(kuò)展等等
- Data Entities:為了固話數(shù)據(jù)到硬盤,數(shù)據(jù)對(duì)象使用Realm移動(dòng)數(shù)據(jù)庫(kù)。
- TwitterAPI:一個(gè)輕量的API實(shí)現(xiàn)來(lái)向Twitter’s JSON API發(fā)送請(qǐng)求。 TwitterAccount是允許您訪問用戶設(shè)備上登錄的Twitter帳戶的類,而TwitterAPI會(huì)向Web JSON端點(diǎn)發(fā)出請(qǐng)求。
- View Models:三個(gè)app的視圖模型位于這里。一個(gè)是功能完整的,你將完成另外兩個(gè)。
- iOS Tweetie: 包含iOS版本的Tweetie,包括一個(gè)storyboard和iOS視圖控制器。
- Mac Tweetie:包含macOS目標(biāo)與它的storyboard,資源和視圖控制器。
- TweetieTests:app測(cè)試和模擬對(duì)象位于此。
Note:直到你完成了章節(jié)挑戰(zhàn)測(cè)試才會(huì)通過,并且你能夠使用測(cè)試來(lái)確保正確完成挑戰(zhàn)。如果現(xiàn)在不工作,不用驚訝!
你的任務(wù)是完成app以便用戶能夠看到在列表中所有用戶的tweets。你首先實(shí)現(xiàn)網(wǎng)絡(luò)層,然后寫一個(gè)視圖模型類,并在最后你將創(chuàng)建兩個(gè)視圖控制器(一個(gè)給iOS,另一個(gè)給macOS),使用完成的視圖模型在屏幕上顯示數(shù)據(jù)。

您將會(huì)參加許多不同的課程,并親身體驗(yàn)MVVM。
Finishing up(完成) the network layer 359
這個(gè)項(xiàng)目已經(jīng)包含了許多代碼。您在本書中已經(jīng)學(xué)了很多,我們不會(huì)來(lái)實(shí)現(xiàn)簡(jiǎn)單的任務(wù),例如設(shè)置observables和視圖控制器。你將開始完成項(xiàng)目的網(wǎng)絡(luò)層。
在TimelineFetcher.swift中的類 TimelineFetcher是負(fù)責(zé)在app連接時(shí)抓取最新的tweets。這個(gè)類很簡(jiǎn)單,并且使用了一個(gè)Rx定時(shí)器來(lái)重復(fù)調(diào)用抓取來(lái)至web的JSON的訂閱。
TimelineFetcher有兩個(gè)遍歷的測(cè)試:一個(gè)用來(lái)抓取給定Twitter列表的推文(tweets),另一個(gè)抓取給定用戶的推文。
在這個(gè)章節(jié),你將增加代碼來(lái)做網(wǎng)絡(luò)請(qǐng)求并映射響應(yīng)到Tweet對(duì)象。在本書中你已經(jīng)完成過相似的任務(wù),因此在Tweet.swift中已經(jīng)包含了大部分代碼。
Note:人們常常會(huì)問當(dāng)使用MVVM做項(xiàng)目時(shí)在哪里增加網(wǎng)絡(luò)層,因此我們編寫了這章讓你有機(jī)會(huì)自己增加網(wǎng)絡(luò)層。關(guān)于網(wǎng)絡(luò)層沒有什么是難以理解的;它是一個(gè)你注入視圖模型的常用類。
在TimelineFetcher.swift, 滾動(dòng)到 init(account:jsonProvider:)的底部,找到這行:
timeline = Observable<[Tweet]>.never()
用以下內(nèi)容替換那行:
timeline = reachableTimerWithAccount
.withLatestFrom(feedCursor.asObservable(), resultSelector:
{ account, cursor in
return (account: account, cursor: cursor)
})
您可以使用定時(shí)器observable reachableTimerWithAccount并將其與feedCursor組合。 feedCursor當(dāng)前沒有做任何事,但是您將使用此變量把你當(dāng)前的位置存儲(chǔ)在Twitter時(shí)間軸中,來(lái)指明您已經(jīng)獲取的哪些推文。
一旦你增加這個(gè)代碼,Xcode會(huì)顯示一個(gè)錯(cuò)誤,現(xiàn)在可以忽略它。這將在增加后續(xù)代碼后解決。
現(xiàn)在增加下面內(nèi)容到鏈:
.flatMapLatest(jsonProvider)
.map(Tweet.unboxMany)
.shareReplayLatestWhileConnected()
您首先將參數(shù)jsonProvider進(jìn)行flatmapping。 jsonProvider是注入到init的閉包。每個(gè)便利inits都支持抓取不同的API端點(diǎn),因此注入 jsonProvider是一個(gè)便利的方式來(lái)避免在主初始化程序 init(account:jsonProvider:)中使用if聲明或分支邏輯。
jsonProvider返回一個(gè) Observable<[JSONObject]>,因此下一步是map到一個(gè) Observable<[Tweet]>。你使用已提供的 Tweet.unboxMany函數(shù),嘗試轉(zhuǎn)換JSON對(duì)象到tweets數(shù)組中。
用這些新的代碼,你準(zhǔn)備抓取tweets了。 timeline是一個(gè)公共的observable,這就是你的視圖模型如何來(lái)訪問最新tweets的列表。app的視圖模型可能存儲(chǔ)了推文到硬盤或馬上(straight away)使用它們驅(qū)動(dòng)app的UI,但是那完全是它們自己的事。 TimelineFetcher簡(jiǎn)單的抓取推文并顯示結(jié)果:

因?yàn)檫@個(gè)訂閱被重復(fù)的調(diào)用,你也需要存儲(chǔ)當(dāng)前位置(或光標(biāo))以便你不會(huì)重復(fù)抓取同樣的推文。接著在你輸入的下面增加:
timeline
.scan(.none, accumulator: TimelineFetcher.currentCursor)
.bindTo(feedCursor)
.addDisposableTo(bag)
feedCursor是在TimelineFetcher上的Variable<TimelineCursor>類型的屬性。 TimelineCursor是一個(gè)自定義結(jié)構(gòu)體,它保存了迄今你已經(jīng)抓取的最新和最老的推文ID。每次你抓取一組新的推文,你就更新 feedCursor的值。如果你對(duì)更新timeline cursot,的邏輯感興趣,請(qǐng)查看 TimelineFetcher.currentCursor()。
Note:本書不覆蓋cursor的詳細(xì)邏輯方面的知識(shí),因?yàn)樗菍S糜赥witter API的。你可以讀取更多關(guān)于cursoring的內(nèi)容在:https://dev.twitter.com/overview/api/cursoring
下一步你需要?jiǎng)?chuàng)建一個(gè)視圖模型。你將使用完成的 TimelineFetcher類從API抓取最新推文。
Adding a View Model 361
本項(xiàng)目已經(jīng)包含了一個(gè)導(dǎo)航類,數(shù)據(jù)實(shí)體,和Twitter賬號(hào)訪問類。現(xiàn)在你的網(wǎng)絡(luò)層已經(jīng)完成,你可以簡(jiǎn)單的合并所有這些給Twitter的登錄用戶,然后抓取一些推文。
在本節(jié),你不用關(guān)心控制器。找到項(xiàng)目的View Models文件夾,打開ListTimelineViewModel.swift。作為同樣的建議,視圖模型將抓取給定用戶列表的推文。
它是一個(gè)很好的實(shí)踐(但確定不是一個(gè)唯一的方式)來(lái)澄清在你的視圖模型代碼的三個(gè)部分的定義:
- Init:在這里你定義一個(gè)或多個(gè)inits來(lái)注入你所有的依賴。
- Input:包含任何公共屬性,例如簡(jiǎn)單(plain)變量或RxSwift主題,它允許視圖控制器提供輸入。
- Output:包含任何公共屬性(通常是observables),它提供視圖模型的輸出。通常有對(duì)象的列表來(lái)驅(qū)動(dòng)一個(gè)表格或集合視圖,或者是一個(gè)視圖控制器用來(lái)驅(qū)動(dòng)app的UI的其他類型的數(shù)據(jù)。

ListTimelineViewModel的初始化里已經(jīng)有少許代碼用來(lái)初始化 fetcher屬性。 fetcher是 TimelineFetcher的一個(gè)實(shí)例,它用來(lái)抓取推文。
是時(shí)候來(lái)增加更多的屬性到視圖模型了。首先,增加下面兩個(gè)屬性,他們既不是輸入又不是輸出,但它簡(jiǎn)單的幫助你持有注入的依賴:
let list: ListIdentifier
let account: Driver<TwitterAccount.AccountStatus>
由于他們是常量,你的唯一初始化他們的機(jī)會(huì)是在 init(account:list:apiType)中。在初始化類頂部插入下面代碼:
self.account = account
self.list = list
現(xiàn)在你能夠繼續(xù)增加輸入屬性。既然你已經(jīng)注入了所有這個(gè)類的依賴,然而什么屬性應(yīng)該做輸入呢?注入依賴和你提供給init的參數(shù)允許你在初始化時(shí)提供給輸入。其他公共屬性將允許你在它生命周期的任何時(shí)候,提供輸入給視圖模型。
例如,考慮一個(gè)讓用戶搜索數(shù)據(jù)庫(kù)的app。你將綁定搜索文本框到視圖模型的輸入屬性。當(dāng)搜索詞改變,視圖模型將響應(yīng)地搜索數(shù)據(jù)庫(kù)并改變他的輸出,它將依次(in turn)綁定到表格視圖來(lái)顯示結(jié)果。
為當(dāng)前的視圖模型,你擁有的唯一輸入是一個(gè)屬性,它讓你暫停和恢復(fù)timeline fetcher類。 TimelineFetcher已經(jīng)具有(feature)一個(gè) Variable<Bool>來(lái)做到這一點(diǎn),所以在視圖模型中你需要一個(gè)代理屬性。
在 ListTimelineViewModel輸入部分,用方便的注釋 // MARK: - Input標(biāo)記的位置,插入下面代碼:
var paused: Bool = false {
didSet {
? fetcher.paused.value = paused
}
這個(gè)屬性是一個(gè)簡(jiǎn)單的代理,它在fetcher類上設(shè)置 paused的值。
現(xiàn)在你能夠繼續(xù)做視圖模型的輸出了。視圖模型將顯示推文的抓取列表和登錄狀態(tài)。前者將是從Realm加載的 Variable的推文對(duì)象;后者,一個(gè) Driver<Bool>簡(jiǎn)單的發(fā)射false或true來(lái)標(biāo)識(shí)是否用戶正確的登錄到Twitter。
在輸出部分(通過注釋標(biāo)記),插入下面兩個(gè)屬性:
private(set) var tweets: Observable<(AnyRealmCollection<Tweet>, RealmChangeset?)>!
private(set) var loggedIn: Driver<Bool>!
tweets包含最新 Tweet對(duì)象的列表。在任何推文被加載前,例如在用戶登錄了他們的Twitter賬號(hào)前,默認(rèn)值是nil。 loggedIn
是一個(gè)Driver,它將在稍后被除數(shù)。
現(xiàn)在你能夠訂閱 TimelineFetcher的結(jié)果,并存儲(chǔ)推文到Realm。當(dāng)你使用RxRealm時(shí),這當(dāng)然是非常容易的。附加到 init(account:list:apiType:):
fetcher.timeline
.subscribe(Realm.rx.add(update: true))
.addDisposableTo(bag)
你訂閱到 fetcher.timeline,它是 Observable<[Tweet]>類型的,然后綁定結(jié)果(tweets的數(shù)組)到 Realm.rx.add(update:)。 Realm.rx.add固化輸入的對(duì)象到app默認(rèn)的Realm數(shù)據(jù)庫(kù)中。
最后一段代碼關(guān)注在你視圖模型中的數(shù)據(jù)流入,所以剩下的就是構(gòu)建視圖模型的輸出。 找到名為bindOutput的方法,然后插入:
guard let realm = try? Realm() else {
return
}
tweets = Observable.changesetFrom(realm.objects(Tweet.self))
當(dāng)你學(xué)習(xí)了21章,“RxRealm”,你可以容易的用Realm的Resultes輔助類來(lái)創(chuàng)建一個(gè)observable序列。在上面的代碼中,您可以從所有持久化的推文中創(chuàng)建一個(gè)結(jié)果集,并訂閱該集合的更改。你呈現(xiàn)感興趣的部分推文observable,它通常是你的試圖控制器。
下一步你需要考慮loggedIn輸出屬性。這個(gè)很容易照顧——你僅僅需要訂閱賬號(hào)并映射它的元素到true或false。附加下面內(nèi)容到bindOutput:
loggedIn = account
.map { status in
switch status {
case .unavailable: return false
case .authorized: return true
}
}
.asDriver(onErrorJustReturn: false)
這是所有視圖模型需要做的!你小心的注入所有依賴到init內(nèi),你增加一些屬性來(lái)允許其他類提供輸入,最后你綁定視圖模型的結(jié)果到公共屬性,這樣其他類就能夠觀察。
正如你看到的,視圖模型不知道任何關(guān)于視圖控制器,視圖,或其他類的內(nèi)容,它們不會(huì)通過視圖模型的初始化注入。因?yàn)橐晥D模型很好的隔離了剩余的代碼,你能夠繼續(xù)寫他的測(cè)試來(lái)確保它正常工作——甚至在你在屏幕上看到任何輸出之前。
Adding a View Model test 364
在Xcode項(xiàng)目導(dǎo)航內(nèi),打開TweetieTests文件夾。在這里面你將找到給你提供的一些東西:
- TestData.swift:提供一些測(cè)試JSON和測(cè)試對(duì)象。
- TwitterTestAPI.swift:Twitter API模擬(mock)類,這個(gè)方法調(diào)用并記錄了API響應(yīng)。
- TestRealm.swift:為了測(cè)試,使用一個(gè)測(cè)試Realm配置確保了Reaml使用一個(gè)零時(shí)的內(nèi)存數(shù)據(jù)庫(kù)。
打開ListTimelineViewModelTests.swift,增加一些新的測(cè)試。這個(gè)類已經(jīng)有一個(gè)實(shí)用的方法來(lái)創(chuàng)建一個(gè)新的 ListTimelineViewModel實(shí)體和兩個(gè)測(cè)試:
- test_whenInitialized_storesInitParams(),它測(cè)試視圖模型是否固化它注入的依賴。
- test_whenInitialized_bindsTweets(),通過它的 tweets屬性,它檢查視圖模型是否顯示最新固化的推文。
為了完成測(cè)試用例,你將增加一個(gè)新的測(cè)試:一個(gè)用來(lái)檢測(cè)是否 loggedIn輸出屬性屬性反應(yīng)了賬號(hào)的鑒定狀態(tài)。增加下面代碼:
func test_whenAccountAvailable_updatesAccountStatus() {
let asyncExpect = expectation(description: "fullfill test")
}
因?yàn)檫@是一個(gè)異步測(cè)試,你定義了一個(gè)expectation,一旦你偵測(cè)到期望的測(cè)試結(jié)果,你將滿足它。
附加下面內(nèi)容到方法中:
let scheduler = TestScheduler(initialClock: 0)
let observer = scheduler.createObserver(Bool.self)
你創(chuàng)建一個(gè)測(cè)試調(diào)度程序(scheduler),然后使用它創(chuàng)建一個(gè)名為observer的測(cè)試觀察者。你將用你的視圖模型的loggedIn屬性測(cè)試元素發(fā)射,因此你可以告訴觀察者來(lái)監(jiān)聽Bool元素。
現(xiàn)在增加下列代碼:
let accountSubject = PublishSubject<TwitterAccount.AccountStatus>()
let viewModel =
createViewModel(accountSubject.asDriver(onErrorJustReturn: .unavailable))
下一步,你創(chuàng)建一個(gè) PublishSubject,你將用來(lái)測(cè)試 AccountStatus值的發(fā)射。你傳遞該主題給 createViewModel(),并最終抓取一個(gè)視圖模型實(shí)例,所有這些都是為測(cè)試做準(zhǔn)備和建立。
下一步你將訂閱在測(cè)試下的observable。增加:
let bag = DisposeBag()
let loggedIn = viewModel.loggedIn.asObservable()
.share()
在這里,您可以獲得可共享的連接,并可以采取一些行動(dòng)。
首先用以下代碼訂閱 loggedIn到測(cè)試觀察者:
loggedIn
.subscribe(observer)
.addDisposableTo(bag)
然后,為了在完成發(fā)送測(cè)試值之后結(jié)束異步測(cè)試,請(qǐng)?zhí)砑樱?/p>
loggedIn
.subscribe(onCompleted: asyncExpect.fulfill)
.addDisposableTo(bag)
現(xiàn)在所有的訂閱在這了,你簡(jiǎn)單的發(fā)射少許測(cè)試值。增加:
accountSubject.onNext(.authorized(TestData.account))
accountSubject.onNext(.unavailable)
accountSubject.onCompleted()
最后,檢查是否 loggedIn發(fā)射了正確的值,增加下面代碼來(lái)比較記錄事件與先前所定義期望的事件列表:
waitForExpectations(timeout: 1.0, handler: { error in
XCTAssertNil(error, error!.localizedDescription)
let expectedEvents = [next(0, true), next(0, false), completed(0)]
XCTAssertEqual(observer.events, expectedEvents)
})
該代碼等待異步期望的實(shí)現(xiàn),然后檢查記錄事件是否是 .next(true), .next(false),和 .completed.的序列。
Note:如果你更愿意,繼續(xù)并使用RxBlocking重寫這個(gè)代碼。你已經(jīng)在16章“Testing with RxTest”中學(xué)到了如何做
接著,測(cè)試用例完成了。高隔離度的視圖模型類讓你容易的注入模擬對(duì)象和仿真輸入。閱讀測(cè)試套件類的其余部分,看看還有什么被測(cè)試。如果你想出一些新的測(cè)試那應(yīng)該是很有用的,隨意增加吧!
Note:應(yīng)為在Tweetie項(xiàng)目的視圖模型非常好的隔離了應(yīng)用基礎(chǔ)的剩余部分,你不需要運(yùn)行整個(gè)應(yīng)用來(lái)運(yùn)行測(cè)試。窺探iOS Tweetie / AppDelegate.swift,查看代碼如何避免在測(cè)試過程中創(chuàng)建應(yīng)用程序的導(dǎo)航和查看控制器?;蛘?,您可以禁用主應(yīng)用程序進(jìn)行測(cè)試。
現(xiàn)在你有了個(gè)全功能的視圖模型,也包括在test。是時(shí)候使用它了!
Adding an iOS View Controller 366
在本節(jié)中,您將編寫代碼,將視圖模型的輸出連接到ListTimelineViewController中的視圖——這個(gè)控制器將在預(yù)設(shè)的列表中顯示組合的用戶的推文。
首先,你將工作在iOS版本的Tweetie上。在這個(gè)項(xiàng)目的導(dǎo)航中,打開iOS Tweetie/View Controllers/List Timeline。在這里面,你將找到試圖控制器和iOS專用的table cell view文件。
打開并瀏覽下ListTimelineViewController.swift。 ListTimelineViewController類具有視圖模型屬性和一個(gè)導(dǎo)航屬性。兩個(gè)類通過靜態(tài)方法createWith(navigator:storyboard:viewModel)被注入。
你將增加兩個(gè)部分啟動(dòng)代碼到視圖控制器。一個(gè)是在 viewDidLoad()中的靜態(tài)配置,另一個(gè)是在 bindUI()中綁定視圖模型到UI。
在 viewDidLoad(),調(diào)用bindUI()之前增加代碼:
title = "@\(viewModel.list.username)/\(viewModel.list.slug)"
navigationItem.rightBarButtonItem =
UIBarButtonItem(barButtonSystemItem: .bookmarks, target: nil, action:
nil)
這將設(shè)置列表的名字作為標(biāo)題并在導(dǎo)航欄右邊創(chuàng)建一個(gè)新按鈕項(xiàng)。
下一步,綁定視圖模型。插入下面代碼到 bindUI():
navigationItem.rightBarButtonItem!.rx.tap
.throttle(0.5, scheduler: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
guard let this = self else { return }
this.navigator.show(segue: .listPeople(this.viewModel.account,
this.viewModel.list), sender: this)
})
.addDisposableTo(bag)
你訂閱右bar項(xiàng)的tap,然后throttle他們來(lái)防止任何雙擊。然后你調(diào)用 navigator屬性的show(segue:sender:)方法來(lái)顯示你呈現(xiàn)到屏幕的segue的意圖。segue顯示人的列表:已經(jīng)選擇Twitter列表成員。
Navigator要么負(fù)責(zé)呈現(xiàn)請(qǐng)求的屏幕,要么丟棄你的意圖,如果它決定執(zhí)行此操作,那么它可能基于其他參數(shù)來(lái)決定忽略你希望呈現(xiàn)視圖控制器的意圖。
Note:通過閱讀Navigator類的定義來(lái)詳細(xì)了解類的實(shí)現(xiàn)。它包含可導(dǎo)航屏幕所有可能的列表,并且您只能通過提供所有必需的輸入?yún)?shù)來(lái)調(diào)用這些segues。
你也需要?jiǎng)?chuàng)建另一個(gè)綁定來(lái)在表格視圖中顯示最新推文。滾動(dòng)到文件頂部,導(dǎo)入下面的庫(kù)可以方便的綁定RxRealm結(jié)果到表格和集合視圖:
import RxRealmDataSources
然后返回到 bindUI()并附加:
let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier:
"TweetCellView", cellType: TweetCellView.self) { cell, _, tweet in
cell.update(with: tweet)
}
dataSource是一個(gè)表格視圖數(shù)據(jù)源,尤其適合驅(qū)動(dòng)來(lái)自Realm集合更改的observable序列的表格視圖。在單一行你配置數(shù)據(jù)源完成:
- 你設(shè)置模型類型為Tweet
- 然后你設(shè)置單元格標(biāo)識(shí)符作為 TweetCellView來(lái)使用
- 最后你提供一個(gè)閉包在它顯示在屏幕上之前來(lái)配置每個(gè)單元
你現(xiàn)在能綁定數(shù)據(jù)資源到視圖控制器的表格視圖。在最后塊的下面增加:
viewModel.tweets
.bindTo(tableView.rx.realmChanges(dataSource))
.addDisposableTo(bag)
在這里你綁定 viewModel.tweets到 realmChanges,并提供預(yù)處理的數(shù)據(jù)源。這是您使用動(dòng)畫更改驅(qū)動(dòng)表格視圖所需的最低限度。
為這個(gè)視圖控制器最后的綁定將依據(jù)是否用戶登錄到Twitter來(lái)決定在頂部顯示或影藏。附加下面代碼
viewModel.loggedIn
.drive(messageView.rx.isHidden)
.addDisposableTo(bag)
這個(gè)綁定開關(guān) messageView.isHidden是基于當(dāng)前 loggedIn的值的。
這部分展示了為什么綁定是MVVM范式的關(guān)鍵。對(duì)于你的視圖控制器它僅作為“膠水”代碼來(lái)服務(wù),這樣你就可以輕松地將問題分開。你的視圖模型保持了大部分關(guān)于當(dāng)前它運(yùn)行的平臺(tái)無(wú)關(guān)的內(nèi)容,英文它不導(dǎo)入任何像UIKit或CocoaUId的框架。
運(yùn)行app并觀察所有你閃亮的新視圖模型所驅(qū)動(dòng)的綁定:

一旦app完成了JSON請(qǐng)求,消息會(huì)在頂部呈現(xiàn)。然后用一個(gè)漂亮的動(dòng)畫來(lái)抓取推文“蜂擁而來(lái)”(pour in)最后,當(dāng)你點(diǎn)擊在右邊的bar item時(shí),app將顯示用戶列表視圖控制器:

那就是!在下一節(jié),你將學(xué)到跨平臺(tái)來(lái)重用你的視圖模型是多么的容易。
Adding a macOS View Controller 369
視圖模型不知道任何關(guān)于視圖或視圖控制器的使用。它的意義是,視圖模型在需要時(shí)是平臺(tái)獨(dú)立的。同樣的視圖模型能容易的提供數(shù)據(jù)給iOS和macOS的視圖控制器。
ListTimelineViewModel恰恰是一個(gè)視圖模型。它僅僅依賴RxSwift, RxCocoa, and the Realm database。因?yàn)檫@些庫(kù)是跨平臺(tái)的,而且視圖模型也是跨平臺(tái)的。
你的工作是切換Xcode項(xiàng)目的macOS目標(biāo),然后構(gòu)造一個(gè)視圖控制器,鏡像你上面構(gòu)筑的iOS的那個(gè)。
從Xcode的scheme選擇MacTweetie/My Mac,然后運(yùn)行項(xiàng)目,看看macOS的起始項(xiàng)目長(zhǎng)什么樣。

這個(gè)app顯示了所有包含在預(yù)定義的推特賬戶的列表,但右邊窗口顯示為空。這個(gè)空的視圖控制器應(yīng)該顯示tweets timeline。當(dāng)完成這個(gè)app,它應(yīng)該非常像你為iOS 推特app創(chuàng)建的推文列表。
打開Mac Tweetie/ViewControllers/List Timeline,選擇ListTimelineViewController.swift。這個(gè)文件名與iOS的視圖控制器文件很相似,但是它是在Mac Tweetie文件夾。
通過顯示在頂部列表的名字開始,就像在iOS app中所做的一樣。增加下面代碼到viewDidLoad():
NSApp.windows.first?.title = "@\(viewModel.list.username)/\
(viewModel.list.slug)"
現(xiàn)在你能夠繼續(xù)專注在綁定上。如果你瀏覽過macOS視圖控制器的代碼,你將注意到像iOS一樣,它使用了同樣的視圖模型和導(dǎo)航類。這是個(gè)好消息,因?yàn)槟阋呀?jīng)知道(和愛)ListTimelineViewModel。
視圖控制器代碼,實(shí)際上,與iOS版本幾乎相同!該代碼相似性是RxSwift的許多好處之一。許多語(yǔ)言的Rx代碼也看起來(lái)很相似。你將驚奇的發(fā)現(xiàn),你能夠閱讀并理解RxJava寫的Java,或是RxJS寫的JavaScript。
與iOS視圖控制器類似,向上滾動(dòng)當(dāng)前文件,并導(dǎo)入RxRealmDataSources::
import RxRealmDataSources
現(xiàn)在滾動(dòng)到bindUI()。綁定視圖模型的推文到表格視圖,增加:
let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier:
"TweetCellView", cellType: TweetCellView.self) { cell, row, tweet in
cell.update(with: tweet)
}
這里你用 TweetCellView標(biāo)識(shí)的單元格創(chuàng)建了一個(gè)包含Tweet對(duì)象的數(shù)據(jù)源,然后通過調(diào)用它上面的 update(with:),在它被重用前,來(lái)配置每個(gè)單元格?,F(xiàn)在創(chuàng)建表格視圖綁定。增加下面代碼:
let binder = tableView.rx.realmChanges(dataSource)
你通過使用已經(jīng)初始化的數(shù)據(jù)源對(duì)象,在表格視圖行和Realm間創(chuàng)建了一個(gè)綁定。
現(xiàn)在你可以簡(jiǎn)單的綁定視圖模型的tweets屬性到配置的綁定。增加下面代碼:
viewModel.tweets
.bindTo(binder)
.addDisposableTo(bag)
這個(gè)綁定讓表格視圖活了。運(yùn)行app并查看在右邊窗口顯示的推文。

這是現(xiàn)實(shí)生活還是幻想? 您不必執(zhí)行任何網(wǎng)絡(luò),數(shù)據(jù)轉(zhuǎn)換或JSON驗(yàn)證?
不,你正在視圖控制器上工作,而不是應(yīng)用程序的其他任何部分。 視圖模型負(fù)責(zé)處理所有內(nèi)容,因此您唯一需要做的是將數(shù)據(jù)綁定到UI。
最后一步來(lái)打磨下表格視圖,因?yàn)樾斜磺袛啵╟ut off)了。你將使用在 NSTableViewDataSource的方法為行設(shè)置制定有高度。因?yàn)槟阏谑褂脭?shù)據(jù)源對(duì)象,你不能直接在你的表格視圖設(shè)置 dataSource。替代方法是,你需要告訴數(shù)據(jù)源對(duì)象你正在提供一些你自己的自定義方法。
向上滾動(dòng)一點(diǎn),然后增增加下面的代碼在你創(chuàng)建binder之前:
dataSource.dataSource = self
現(xiàn)在你需要讓 ListTimelineViewController遵循 NSTableViewDataSource,然后增加這個(gè)方法來(lái)設(shè)置表格視圖的高度。增加下面代碼到文件的底部:
extension ListTimelineViewController: NSTableViewDataSource {
func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
return 68.0
}
}
你實(shí)現(xiàn)了 tableView(_:heightOfRow:),然后返回你想要的行高——68點(diǎn)。如果你喜歡自定義其他部分來(lái)呈現(xiàn)你的表格視圖,你可以增加其他 NSTableViewDataSource方法,或者設(shè)置數(shù)據(jù)源的 delegate屬性,然后增加 NSTableViewDelegate方法到視圖控制器。
再一次運(yùn)行這個(gè)app,然后你將看到表格看起來(lái)更好:

現(xiàn)在,你對(duì)于如何從視圖控制器分離你的代碼到模型,視圖模型和視圖有了一個(gè)基本的概念。除了簡(jiǎn)單的應(yīng)用程序之外,MVVM肯定對(duì)MVC有好處,但重要的是要記住,MVVM并不是唯一的選擇。
MVVM是與RxSwift一起使用的特別甜蜜的模式,因?yàn)镽x使創(chuàng)建綁定成為一項(xiàng)簡(jiǎn)單的任務(wù)。 這導(dǎo)致更簡(jiǎn)單的代碼,更容易閱讀和測(cè)試。
其他構(gòu)架樣式有不同的優(yōu)點(diǎn),其他的庫(kù)可能更適合這些樣式。但是如果您將MVVM + RxSwift視為您可能想要學(xué)習(xí)的東西,那么肯定會(huì)嘗試下面的挑戰(zhàn)。

