RxSwift_v1.0筆記——23 MVVM with RxSwift

RxSwift_v1.0筆記——23 MVVM with RxSwift

RxSwift是一個很大的話題,本書之前沒有覆蓋任何應用構架的細節。是因為RxSwift不會強迫在你的應用上使用任何特定的構架。不過,因為RxSwift與MVVM一起工作更合適,本章將專注于討論討論特殊的構架樣式。

Introducing MVVM 353

MVVM代表了Model-View-ViewModel;它與Apple的親兒子MVC有略微不同的實現。

用一個開放的思想來處理MVVM是很重要的。MVVM不是軟件構架的萬能藥;當然,考慮到MVVM是一個軟件范式,使用它是朝著好的應用構架邁出的第一步,尤其是你開始是MVC的思維方式。

Some background on MVC 354

現在你對MVVM和MVC可能感覺有一點矛盾(tension)。它們之間有什么聯系?它們非常相似,甚至你可以認為它們是遠房親戚。但是解釋它們之間的不同點任然是必要的。

在本書(和其他關于編程的書)中的大部分的例子使用MVC樣式來寫代碼示例。MVC是對許多簡單的app來說是簡單的樣式,它看起來像這樣:

每個類分配一個類別:controller類扮演中間角色讓model和view能夠更新,views僅僅在屏幕上顯示數據并發送事件(如手勢)到controller。最后models讀和寫數據來固化app狀態。

MVC是簡單的樣式,它能暫時(for a while)為你服務,但是當你的app成長后,你將注意到許多類既不是view又不是model,所以只能是controllers。你開始調入一個普遍的陷阱,在一個controller類中增加了越來越多的代碼。由于你是從iOS應用程序啟動視圖控制器,最簡單的方法是將所有代碼放入該視圖控制器類。因此,MVC代表“Massive View Controller”的老笑話,因為控制器可以成長為數百甚至數千條行。

過載你的類是一個不好的做法,但不一定是MVC模式的缺點。例如:蘋果的許多開發人員都是MVC的粉絲,他們生產(turn out)了非常好的macOS和iOS軟件。

Note:你可以閱讀更多關于MVC在蘋果專用的文檔頁面:https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

MVVM to the rescue 354

MVVM看起來很像MVC,但一定感覺更好。喜歡MVC的人通常也喜歡MVVM,這個新的樣式讓他們更容易解決許多MVC普遍的問題。

與MVC明顯不同的是一個叫ViewModel的新種類:

ViewModel在構架中作為一個核心角色:它負責業務邏輯并與模型和視圖進行對話。

MVVM有以下簡單的規則:

  • Models不直接與其他類對話,但是他們能發射關于數據變化的通知。
  • View ModelsModels對話并暴露數據給View Controllers
  • View Controllers僅僅與View ModelsViews會話,他們處理視圖的生命周期并綁定數據到UI組件。
  • Views僅僅通知事件給視圖控制器(就像MVC一樣)。

等等,View Model不正是做了MVC中控制器做的事嗎?是,也不是。

正如先前所說的,普遍的問題是視圖控制器塞入了不是控制視圖的代碼。MVVM通過把視圖控制器與視圖組合來嘗試解決這個問題,并讓它只負責控制視圖。

MVVM構架的另一個好處是增加代碼的可測試性。把視圖的生命周期從業務邏輯分離,讓測試視圖控制器和視圖模型變的非常簡單。

最后但并非最不重要的是,視圖模型完全從顯示層分離,當需要的時候,能夠在不同平臺間重用。你可以僅僅替換視圖/視圖控制器對,然后遷移你的app從iOS到macOS,甚至是tvOS。

What goes where? 355

但是,不要以為一切都應該在你的View Model類中。

這與你最終在MVC一樣,將是同樣愚蠢的。您可以基于你的代碼來明確劃分和分配責任。因此,留著View Model作為數據和屏幕之間的大腦,但是確保你分離了網絡,導航,緩存和相似的職責到其他類。

如果它們不屬于任何MVVM類別,這些額外的類如何處理呢?MVVM對于這些沒有硬性規定,但是在本章你將工作的項目,它將給你介紹一些可行的解決方案。

本章將介紹一個好方法,它將通過其初始化或盡可能在其生命周期之后,注入View Model所需的所有對象。這就是說你能夠將長時間活動的對象,像是API類的狀態或來自視圖模型的固化層對象到另一個視圖模型:

在本章的項目“Tweetie”中,您將以這種方式傳遞一些東西,例如關于應用內導航(Navigator)的對象,當前登錄的Twitter帳戶(TwitterAPI.AccountStatus)等等。

但是MVVM的唯一好處是讓代碼變的更短嗎?如果使用得當,MVVM比MVC有更多優勢:

  • 視圖控制器趨于更簡單和名副其實,因為它的唯一責任是控制視圖。MVVM更易于使用RxSwift/RxCocoa,因為能夠綁定observables到UI組件是MVVM的關鍵能力。
  • 視圖模型有清晰的Input -> Output樣式,并且在為預期輸出提供預定義的輸入和測試時非常容易測試。
  • 通過創建模擬視圖模型并對預期的視圖控制器狀態進行測試,形象化地測試視圖控制器變得更加容易。

最后但并非最不重要的一點,因為MVVM是一個偉大的分離至MVC,它也可以作為一個啟發和靈感來探索更多的軟件架構模式。

想試試MVVM? 在您閱讀本章時,您將看到其許多好處。

Getting started with Tweetie 357

本章,你將工作在叫做Tweetie的多平臺項目。它是一個非常簡單的Twitter-powered應用,它使用一個預定義的用戶列表來向用戶顯示推文。默認情況下,起始程序項目使用的是具有(featuring)本書所有作者和編輯者的Twitter列表。如果你喜歡,你可以很容易的改變這個列表來轉換項目為運動,寫作,攝影app。

這個項目面向macOS和iOS,并通過使用MVVM樣式來解決許多現實生活(real-life)中的編程任務。有許多代碼包含在了起始項目中,你將聚焦在MVVM相關部分。

當你完成本章后,你將見證MVVM有助于區分以下內容:

  • 與UI相關的代碼是特定于平臺的,例如為iOS的視圖控制器使用UIKit ,以及分離的macOS獨有的視圖控制器使用Cocoa。
  • 代碼按原樣重用,因為它不依賴于特定平臺的UI框架,例如模型和視圖模型中的所有代碼。

是時候潛入了!

Project structure 357

安裝所有CocoaPods,打開項目,預覽下項目結構。

在項目導航中,你將發現有很多文件夾:

  • Common Classes:在macOS與iOS間共享代碼。包含一個RxReachability類擴展,在 UITableView, NSTableView上的擴展等等
  • Data Entities:為了固話數據到硬盤,數據對象使用Realm移動數據庫。
  • TwitterAPI:一個輕量的API實現來向Twitter’s JSON API發送請求。 TwitterAccount是允許您訪問用戶設備上登錄的Twitter帳戶的類,而TwitterAPI會向Web JSON端點發出請求。
  • View Models:三個app的視圖模型位于這里。一個是功能完整的,你將完成另外兩個。
  • iOS Tweetie: 包含iOS版本的Tweetie,包括一個storyboard和iOS視圖控制器。
  • Mac Tweetie:包含macOS目標與它的storyboard,資源和視圖控制器。
  • TweetieTests:app測試和模擬對象位于此。
Note:直到你完成了章節挑戰測試才會通過,并且你能夠使用測試來確保正確完成挑戰。如果現在不工作,不用驚訝!

你的任務是完成app以便用戶能夠看到在列表中所有用戶的tweets。你首先實現網絡層,然后寫一個視圖模型類,并在最后你將創建兩個視圖控制器(一個給iOS,另一個給macOS),使用完成的視圖模型在屏幕上顯示數據。

您將會參加許多不同的課程,并親身體驗MVVM。

Finishing up(完成) the network layer 359

這個項目已經包含了許多代碼。您在本書中已經學了很多,我們不會來實現簡單的任務,例如設置observables和視圖控制器。你將開始完成項目的網絡層。

TimelineFetcher.swift中的類 TimelineFetcher是負責在app連接時抓取最新的tweets。這個類很簡單,并且使用了一個Rx定時器來重復調用抓取來至web的JSON的訂閱。

TimelineFetcher有兩個遍歷的測試:一個用來抓取給定Twitter列表的推文(tweets),另一個抓取給定用戶的推文。

在這個章節,你將增加代碼來做網絡請求并映射響應到Tweet對象。在本書中你已經完成過相似的任務,因此在Tweet.swift中已經包含了大部分代碼。

Note:人們常常會問當使用MVVM做項目時在哪里增加網絡層,因此我們編寫了這章讓你有機會自己增加網絡層。關于網絡層沒有什么是難以理解的;它是一個你注入視圖模型的常用類。

TimelineFetcher.swift, 滾動到 init(account:jsonProvider:)的底部,找到這行:

timeline = Observable<[Tweet]>.never()

用以下內容替換那行:

timeline = reachableTimerWithAccount
  .withLatestFrom(feedCursor.asObservable(), resultSelector:
    { account, cursor in
      return (account: account, cursor: cursor)
  })

您可以使用定時器observable reachableTimerWithAccount并將其與feedCursor組合。 feedCursor當前沒有做任何事,但是您將使用此變量把你當前的位置存儲在Twitter時間軸中,來指明您已經獲取的哪些推文。

一旦你增加這個代碼,Xcode會顯示一個錯誤,現在可以忽略它。這將在增加后續代碼后解決。

現在增加下面內容到鏈:

.flatMapLatest(jsonProvider)
.map(Tweet.unboxMany)
.shareReplayLatestWhileConnected()

您首先將參數jsonProvider進行flatmapping。 jsonProvider是注入到init的閉包。每個便利inits都支持抓取不同的API端點,因此注入 jsonProvider是一個便利的方式來避免在主初始化程序 init(account:jsonProvider:)中使用if聲明或分支邏輯。

jsonProvider返回一個 Observable<[JSONObject]>,因此下一步是map到一個 Observable<[Tweet]>。你使用已提供的 Tweet.unboxMany函數,嘗試轉換JSON對象到tweets數組中。

用這些新的代碼,你準備抓取tweets了。 timeline是一個公共的observable,這就是你的視圖模型如何來訪問最新tweets的列表。app的視圖模型可能存儲了推文到硬盤或馬上(straight away)使用它們驅動app的UI,但是那完全是它們自己的事。 TimelineFetcher簡單的抓取推文并顯示結果:

因為這個訂閱被重復的調用,你也需要存儲當前位置(或光標)以便你不會重復抓取同樣的推文。接著在你輸入的下面增加:

timeline
  .scan(.none, accumulator: TimelineFetcher.currentCursor)
  .bindTo(feedCursor)
  .addDisposableTo(bag)

feedCursor是在TimelineFetcher上的Variable<TimelineCursor>類型的屬性。 TimelineCursor是一個自定義結構體,它保存了迄今你已經抓取的最新和最老的推文ID。每次你抓取一組新的推文,你就更新 feedCursor的值。如果你對更新timeline cursot,的邏輯感興趣,請查看 TimelineFetcher.currentCursor()。

Note:本書不覆蓋cursor的詳細邏輯方面的知識,因為他是專用于Twitter API的。你可以讀取更多關于cursoring的內容在:https://dev.twitter.com/overview/api/cursoring

下一步你需要創建一個視圖模型。你將使用完成的 TimelineFetcher類從API抓取最新推文。

Adding a View Model 361

本項目已經包含了一個導航類,數據實體,和Twitter賬號訪問類。現在你的網絡層已經完成,你可以簡單的合并所有這些給Twitter的登錄用戶,然后抓取一些推文。

在本節,你不用關心控制器。找到項目的View Models文件夾,打開ListTimelineViewModel.swift。作為同樣的建議,視圖模型將抓取給定用戶列表的推文。

它是一個很好的實踐(但確定不是一個唯一的方式)來澄清在你的視圖模型代碼的三個部分的定義:

  1. Init:在這里你定義一個或多個inits來注入你所有的依賴。
  2. Input:包含任何公共屬性,例如簡單(plain)變量或RxSwift主題,它允許視圖控制器提供輸入。
  3. Output:包含任何公共屬性(通常是observables),它提供視圖模型的輸出。通常有對象的列表來驅動一個表格或集合視圖,或者是一個視圖控制器用來驅動app的UI的其他類型的數據。

ListTimelineViewModel的初始化里已經有少許代碼用來初始化 fetcher屬性。 fetcher是 TimelineFetcher的一個實例,它用來抓取推文。

是時候來增加更多的屬性到視圖模型了。首先,增加下面兩個屬性,他們既不是輸入又不是輸出,但它簡單的幫助你持有注入的依賴:

let list: ListIdentifier
let account: Driver<TwitterAccount.AccountStatus>

由于他們是常量,你的唯一初始化他們的機會是在 init(account:list:apiType)中。在初始化類頂部插入下面代碼:

self.account = account
self.list = list

現在你能夠繼續增加輸入屬性。既然你已經注入了所有這個類的依賴,然而什么屬性應該做輸入呢?注入依賴和你提供給init的參數允許你在初始化時提供給輸入。其他公共屬性將允許你在它生命周期的任何時候,提供輸入給視圖模型。

例如,考慮一個讓用戶搜索數據庫的app。你將綁定搜索文本框到視圖模型的輸入屬性。當搜索詞改變,視圖模型將響應地搜索數據庫并改變他的輸出,它將依次(in turn)綁定到表格視圖來顯示結果。

為當前的視圖模型,你擁有的唯一輸入是一個屬性,它讓你暫停和恢復timeline fetcher類。 TimelineFetcher已經具有(feature)一個 Variable<Bool>來做到這一點,所以在視圖模型中你需要一個代理屬性。

在 ListTimelineViewModel輸入部分,用方便的注釋 // MARK: - Input標記的位置,插入下面代碼:

var paused: Bool = false {
didSet {
? fetcher.paused.value = paused
}

這個屬性是一個簡單的代理,它在fetcher類上設置 paused的值。

現在你能夠繼續做視圖模型的輸出了。視圖模型將顯示推文的抓取列表和登錄狀態。前者將是從Realm加載的 Variable的推文對象;后者,一個 Driver<Bool>簡單的發射false或true來標識是否用戶正確的登錄到Twitter。

在輸出部分(通過注釋標記),插入下面兩個屬性:

  private(set) var tweets: Observable<(AnyRealmCollection<Tweet>, RealmChangeset?)>!
  private(set) var loggedIn: Driver<Bool>!

tweets包含最新 Tweet對象的列表。在任何推文被加載前,例如在用戶登錄了他們的Twitter賬號前,默認值是nil。 loggedIn

是一個Driver,它將在稍后被除數。

現在你能夠訂閱 TimelineFetcher的結果,并存儲推文到Realm。當你使用RxRealm時,這當然是非常容易的。附加到 init(account:list:apiType:):

fetcher.timeline
  .subscribe(Realm.rx.add(update: true))
  .addDisposableTo(bag)

你訂閱到 fetcher.timeline,它是 Observable<[Tweet]>類型的,然后綁定結果(tweets的數組)到 Realm.rx.add(update:)。 Realm.rx.add固化輸入的對象到app默認的Realm數據庫中。

最后一段代碼關注在你視圖模型中的數據流入,所以剩下的就是構建視圖模型的輸出。 找到名為bindOutput的方法,然后插入:

guard let realm = try? Realm() else {
  return
}
tweets = Observable.changesetFrom(realm.objects(Tweet.self))

當你學習了21章,“RxRealm”,你可以容易的用Realm的Resultes輔助類來創建一個observable序列。在上面的代碼中,您可以從所有持久化的推文中創建一個結果集,并訂閱該集合的更改。你呈現感興趣的部分推文observable,它通常是你的試圖控制器。

下一步你需要考慮loggedIn輸出屬性。這個很容易照顧——你僅僅需要訂閱賬號并映射它的元素到true或false。附加下面內容到bindOutput:

loggedIn = account
  .map { status in
    switch status {
    case .unavailable: return false
    case .authorized: return true
    }
  }
  .asDriver(onErrorJustReturn: false)

這是所有視圖模型需要做的!你小心的注入所有依賴到init內,你增加一些屬性來允許其他類提供輸入,最后你綁定視圖模型的結果到公共屬性,這樣其他類就能夠觀察。

正如你看到的,視圖模型不知道任何關于視圖控制器,視圖,或其他類的內容,它們不會通過視圖模型的初始化注入。因為視圖模型很好的隔離了剩余的代碼,你能夠繼續寫他的測試來確保它正常工作——甚至在你在屏幕上看到任何輸出之前。

Adding a View Model test 364

在Xcode項目導航內,打開TweetieTests文件夾。在這里面你將找到給你提供的一些東西:

  • TestData.swift:提供一些測試JSON和測試對象。
  • TwitterTestAPI.swift:Twitter API模擬(mock)類,這個方法調用并記錄了API響應。
  • TestRealm.swift:為了測試,使用一個測試Realm配置確保了Reaml使用一個零時的內存數據庫。

打開ListTimelineViewModelTests.swift,增加一些新的測試。這個類已經有一個實用的方法來創建一個新的 ListTimelineViewModel實體和兩個測試:

  1. test_whenInitialized_storesInitParams(),它測試視圖模型是否固化它注入的依賴。
  2. test_whenInitialized_bindsTweets(),通過它的 tweets屬性,它檢查視圖模型是否顯示最新固化的推文。

為了完成測試用例,你將增加一個新的測試:一個用來檢測是否 loggedIn輸出屬性屬性反應了賬號的鑒定狀態。增加下面代碼:

func test_whenAccountAvailable_updatesAccountStatus() {
  let asyncExpect = expectation(description: "fullfill test")
}

因為這是一個異步測試,你定義了一個expectation,一旦你偵測到期望的測試結果,你將滿足它。

附加下面內容到方法中:

let scheduler = TestScheduler(initialClock: 0)
let observer = scheduler.createObserver(Bool.self)

你創建一個測試調度程序(scheduler),然后使用它創建一個名為observer的測試觀察者。你將用你的視圖模型的loggedIn屬性測試元素發射,因此你可以告訴觀察者來監聽Bool元素。

現在增加下列代碼:

let accountSubject = PublishSubject<TwitterAccount.AccountStatus>()
let viewModel =
createViewModel(accountSubject.asDriver(onErrorJustReturn: .unavailable))

下一步,你創建一個 PublishSubject,你將用來測試 AccountStatus值的發射。你傳遞該主題給 createViewModel(),并最終抓取一個視圖模型實例,所有這些都是為測試做準備和建立。

下一步你將訂閱在測試下的observable。增加:

let bag = DisposeBag()
let loggedIn = viewModel.loggedIn.asObservable()
  .share()

在這里,您可以獲得可共享的連接,并可以采取一些行動。

首先用以下代碼訂閱 loggedIn到測試觀察者:

loggedIn
  .subscribe(observer)
  .addDisposableTo(bag)

然后,為了在完成發送測試值之后結束異步測試,請添加:

loggedIn
  .subscribe(onCompleted: asyncExpect.fulfill)
  .addDisposableTo(bag)

現在所有的訂閱在這了,你簡單的發射少許測試值。增加:

accountSubject.onNext(.authorized(TestData.account))
accountSubject.onNext(.unavailable)
accountSubject.onCompleted()

最后,檢查是否 loggedIn發射了正確的值,增加下面代碼來比較記錄事件與先前所定義期望的事件列表:

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)
})

該代碼等待異步期望的實現,然后檢查記錄事件是否是 .next(true), .next(false),和 .completed.的序列。

Note:如果你更愿意,繼續并使用RxBlocking重寫這個代碼。你已經在16章“Testing with RxTest”中學到了如何做

接著,測試用例完成了。高隔離度的視圖模型類讓你容易的注入模擬對象和仿真輸入。閱讀測試套件類的其余部分,看看還有什么被測試。如果你想出一些新的測試那應該是很有用的,隨意增加吧!

Note:應為在Tweetie項目的視圖模型非常好的隔離了應用基礎的剩余部分,你不需要運行整個應用來運行測試。窺探iOS Tweetie / AppDelegate.swift,查看代碼如何避免在測試過程中創建應用程序的導航和查看控制器。或者,您可以禁用主應用程序進行測試。

現在你有了個全功能的視圖模型,也包括在test。是時候使用它了!

Adding an iOS View Controller 366

在本節中,您將編寫代碼,將視圖模型的輸出連接到ListTimelineViewController中的視圖——這個控制器將在預設的列表中顯示組合的用戶的推文。

首先,你將工作在iOS版本的Tweetie上。在這個項目的導航中,打開iOS Tweetie/View Controllers/List Timeline。在這里面,你將找到試圖控制器和iOS專用的table cell view文件。

打開并瀏覽下ListTimelineViewController.swift。 ListTimelineViewController類具有視圖模型屬性和一個導航屬性。兩個類通過靜態方法createWith(navigator:storyboard:viewModel)被注入。

你將增加兩個部分啟動代碼到視圖控制器。一個是在 viewDidLoad()中的靜態配置,另一個是在 bindUI()中綁定視圖模型到UI。

在 viewDidLoad(),調用bindUI()之前增加代碼:

title = "@\(viewModel.list.username)/\(viewModel.list.slug)"
navigationItem.rightBarButtonItem =
UIBarButtonItem(barButtonSystemItem: .bookmarks, target: nil, action:
nil)

這將設置列表的名字作為標題并在導航欄右邊創建一個新按鈕項。

下一步,綁定視圖模型。插入下面代碼到 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項的tap,然后throttle他們來防止任何雙擊。然后你調用 navigator屬性的show(segue:sender:)方法來顯示你呈現到屏幕的segue的意圖。segue顯示人的列表:已經選擇Twitter列表成員。

Navigator要么負責呈現請求的屏幕,要么丟棄你的意圖,如果它決定執行此操作,那么它可能基于其他參數來決定忽略你希望呈現視圖控制器的意圖。

Note:通過閱讀Navigator類的定義來詳細了解類的實現。它包含可導航屏幕所有可能的列表,并且您只能通過提供所有必需的輸入參數來調用這些segues。

你也需要創建另一個綁定來在表格視圖中顯示最新推文。滾動到文件頂部,導入下面的庫可以方便的綁定RxRealm結果到表格和集合視圖:

import RxRealmDataSources

然后返回到 bindUI()并附加:

let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier:
  "TweetCellView", cellType: TweetCellView.self) { cell, _, tweet in
  cell.update(with: tweet)
}

dataSource是一個表格視圖數據源,尤其適合驅動來自Realm集合更改的observable序列的表格視圖。在單一行你配置數據源完成:

  1. 你設置模型類型為Tweet
  2. 然后你設置單元格標識符作為 TweetCellView來使用
  3. 最后你提供一個閉包在它顯示在屏幕上之前來配置每個單元

你現在能綁定數據資源到視圖控制器的表格視圖。在最后塊的下面增加:

viewModel.tweets
  .bindTo(tableView.rx.realmChanges(dataSource))
  .addDisposableTo(bag)

在這里你綁定 viewModel.tweets到 realmChanges,并提供預處理的數據源。這是您使用動畫更改驅動表格視圖所需的最低限度。

為這個視圖控制器最后的綁定將依據是否用戶登錄到Twitter來決定在頂部顯示或影藏。附加下面代碼

viewModel.loggedIn
  .drive(messageView.rx.isHidden)
  .addDisposableTo(bag)

這個綁定開關 messageView.isHidden是基于當前 loggedIn的值的。

這部分展示了為什么綁定是MVVM范式的關鍵。對于你的視圖控制器它僅作為“膠水”代碼來服務,這樣你就可以輕松地將問題分開。你的視圖模型保持了大部分關于當前它運行的平臺無關的內容,英文它不導入任何像UIKit或CocoaUId的框架。

運行app并觀察所有你閃亮的新視圖模型所驅動的綁定:

一旦app完成了JSON請求,消息會在頂部呈現。然后用一個漂亮的動畫來抓取推文“蜂擁而來”(pour in)最后,當你點擊在右邊的bar item時,app將顯示用戶列表視圖控制器:

那就是!在下一節,你將學到跨平臺來重用你的視圖模型是多么的容易。

Adding a macOS View Controller 369

視圖模型不知道任何關于視圖或視圖控制器的使用。它的意義是,視圖模型在需要時是平臺獨立的。同樣的視圖模型能容易的提供數據給iOS和macOS的視圖控制器。

ListTimelineViewModel恰恰是一個視圖模型。它僅僅依賴RxSwift, RxCocoa, and the Realm database。因為這些庫是跨平臺的,而且視圖模型也是跨平臺的。

你的工作是切換Xcode項目的macOS目標,然后構造一個視圖控制器,鏡像你上面構筑的iOS的那個。

從Xcode的scheme選擇MacTweetie/My Mac,然后運行項目,看看macOS的起始項目長什么樣。

這個app顯示了所有包含在預定義的推特賬戶的列表,但右邊窗口顯示為空。這個空的視圖控制器應該顯示tweets timeline。當完成這個app,它應該非常像你為iOS 推特app創建的推文列表。

打開Mac Tweetie/ViewControllers/List Timeline,選擇ListTimelineViewController.swift。這個文件名與iOS的視圖控制器文件很相似,但是它是在Mac Tweetie文件夾。

通過顯示在頂部列表的名字開始,就像在iOS app中所做的一樣。增加下面代碼到viewDidLoad():

NSApp.windows.first?.title = "@\(viewModel.list.username)/\
(viewModel.list.slug)"

現在你能夠繼續專注在綁定上。如果你瀏覽過macOS視圖控制器的代碼,你將注意到像iOS一樣,它使用了同樣的視圖模型和導航類。這是個好消息,因為你已經知道(和愛)ListTimelineViewModel。

視圖控制器代碼,實際上,與iOS版本幾乎相同!該代碼相似性是RxSwift的許多好處之一。許多語言的Rx代碼也看起來很相似。你將驚奇的發現,你能夠閱讀并理解RxJava寫的Java,或是RxJS寫的JavaScript。

與iOS視圖控制器類似,向上滾動當前文件,并導入RxRealmDataSources::

import RxRealmDataSources

現在滾動到bindUI()。綁定視圖模型的推文到表格視圖,增加:

let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier: 
    "TweetCellView", cellType: TweetCellView.self) { cell, row, tweet in
  cell.update(with: tweet)
}

這里你用 TweetCellView標識的單元格創建了一個包含Tweet對象的數據源,然后通過調用它上面的 update(with:),在它被重用前,來配置每個單元格。現在創建表格視圖綁定。增加下面代碼:

let binder = tableView.rx.realmChanges(dataSource)

你通過使用已經初始化的數據源對象,在表格視圖行和Realm間創建了一個綁定。

現在你可以簡單的綁定視圖模型的tweets屬性到配置的綁定。增加下面代碼:

viewModel.tweets
  .bindTo(binder)
  .addDisposableTo(bag) 

這個綁定讓表格視圖活了。運行app并查看在右邊窗口顯示的推文。

這是現實生活還是幻想? 您不必執行任何網絡,數據轉換或JSON驗證?

不,你正在視圖控制器上工作,而不是應用程序的其他任何部分。 視圖模型負責處理所有內容,因此您唯一需要做的是將數據綁定到UI。

最后一步來打磨下表格視圖,因為行被切斷(cut off)了。你將使用在 NSTableViewDataSource的方法為行設置制定有高度。因為你正在使用數據源對象,你不能直接在你的表格視圖設置 dataSource。替代方法是,你需要告訴數據源對象你正在提供一些你自己的自定義方法。

向上滾動一點,然后增增加下面的代碼在你創建binder之前:

dataSource.dataSource = self

現在你需要讓 ListTimelineViewController遵循 NSTableViewDataSource,然后增加這個方法來設置表格視圖的高度。增加下面代碼到文件的底部:

extension ListTimelineViewController: NSTableViewDataSource {
  func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    return 68.0
  }
}

你實現了 tableView(_:heightOfRow:),然后返回你想要的行高——68點。如果你喜歡自定義其他部分來呈現你的表格視圖,你可以增加其他 NSTableViewDataSource方法,或者設置數據源的 delegate屬性,然后增加 NSTableViewDelegate方法到視圖控制器。

再一次運行這個app,然后你將看到表格看起來更好:

現在,你對于如何從視圖控制器分離你的代碼到模型,視圖模型和視圖有了一個基本的概念。除了簡單的應用程序之外,MVVM肯定對MVC有好處,但重要的是要記住,MVVM并不是唯一的選擇。

MVVM是與RxSwift一起使用的特別甜蜜的模式,因為Rx使創建綁定成為一項簡單的任務。 這導致更簡單的代碼,更容易閱讀和測試。

其他構架樣式有不同的優點,其他的庫可能更適合這些樣式。但是如果您將MVVM + RxSwift視為您可能想要學習的東西,那么肯定會嘗試下面的挑戰。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容