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 Models與Models對話并暴露數據給View Controllers。
- View Controllers僅僅與View Models和Views會話,他們處理視圖的生命周期并綁定數據到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。作為同樣的建議,視圖模型將抓取給定用戶列表的推文。
它是一個很好的實踐(但確定不是一個唯一的方式)來澄清在你的視圖模型代碼的三個部分的定義:
- Init:在這里你定義一個或多個inits來注入你所有的依賴。
- Input:包含任何公共屬性,例如簡單(plain)變量或RxSwift主題,它允許視圖控制器提供輸入。
- 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實體和兩個測試:
- test_whenInitialized_storesInitParams(),它測試視圖模型是否固化它注入的依賴。
- 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序列的表格視圖。在單一行你配置數據源完成:
- 你設置模型類型為Tweet
- 然后你設置單元格標識符作為 TweetCellView來使用
- 最后你提供一個閉包在它顯示在屏幕上之前來配置每個單元
你現在能綁定數據資源到視圖控制器的表格視圖。在最后塊的下面增加:
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視為您可能想要學習的東西,那么肯定會嘗試下面的挑戰。