RxSwift_v1.0筆記——23 MVVM with RxSwift

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 ModelsModels對(duì)話并暴露數(shù)據(jù)給View Controllers。
  • View Controllers僅僅與View ModelsViews會(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è)部分的定義:

  1. Init:在這里你定義一個(gè)或多個(gè)inits來(lái)注入你所有的依賴。
  2. Input:包含任何公共屬性,例如簡(jiǎn)單(plain)變量或RxSwift主題,它允許視圖控制器提供輸入。
  3. 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è)試:

  1. test_whenInitialized_storesInitParams(),它測(cè)試視圖模型是否固化它注入的依賴。
  2. 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ù)源完成:

  1. 你設(shè)置模型類型為Tweet
  2. 然后你設(shè)置單元格標(biāo)識(shí)符作為 TweetCellView來(lái)使用
  3. 最后你提供一個(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)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容