[譯] iOS 裡的 MVVM 和 RxSwift

iWeslie發表於2019-04-18

在本文中,我將介紹 iOS 程式設計中的 MVVM 設計模式以及 RxSwift。本文分為兩部分,第一部分簡要介紹了設計模式和 RxSwift 的基礎知識,而在 第二部分 裡,有一個實現了 MVVM 和 RxSwift 的示例專案。

設計模式:


首先,我們為什麼要使用設計模式呢?簡而言之,就是為了避免我們的程式碼亂成一團,當然這不是唯一的原因,其中有一個原因是可測試性。設計模式有很多,我們可以指出幾個非常受歡迎的模式:MVC、MVVM、MVPVIPER。下面的圖片將這幾個設計模式的分佈協作性,可測試性和易用性進行了比較。

Compare of design patterns ( from NSLondon )

這些設計模式都有自己的優缺點,但最終它們都能使我們的程式碼更清晰、簡單並且易於閱讀。本文重點介紹 MVVM,我希望你能在閱讀完 第二部分 後著手實現它。

讓我們簡單介紹一下 MVC,然後繼續討論 MVVM


MVC:

蘋果官方建議使用 MVC 進行 iOS 程式設計,如果你有一定的 iOS 開發經驗,你可能會熟悉 MVC。這個模式由 Model、ViewController 組成,其中 Controller 負責將 Model 連線到 View。理論上看起來 View 和 Controller 是兩個不同的東西,但在 iOS 的世界中,不幸的是,大多數情況下它們是一回事。當然,在小型專案中,一切似乎都符合規律,但是一旦你的專案變得龐大,Controller 因實現了大部分業務邏輯而變得臃腫,這會導致程式碼變得混亂,但是如果你能正確編寫 MVC,並儘可能地把 Controller 裡的東西解耦,大多數情況下這個問題將得到解決。

MVC from apple docs
官方文件中的 MVC

MVVM:

picture from github

MVVM 代表 Model、ViewViewModel,其中,View 和業務邏輯實現了 Controller,View 以及動畫,ViewModel 裡則是 api 的呼叫。實際上 ViewModel 這層是 Model 和 View 之間的介面並且它給 View 提供資料。有一點要注意的是,如果你在 ViewModel 的檔案中看到以下程式碼,那你可能是在某處犯了一個錯誤:

import UIKit
複製程式碼

這是因為 ViewModel 不應該和 View 有任何牽連,在 第二部分 中我們將藉助一個例子來研究這篇文章。

RxSwift:


MVVM 的一個特性是資料和 View 的繫結,而 RxSwift 就很完美地實現了這一點。當然,您也可以使用 delegate,KVO 或閉包執行此操作,但 Rx 的有一個特性就是,它是一種思想,在很多語言裡通用,因此它與程式語言關係並不大。你可以在 這裡 找到它支援的語言列表。現在在這一部分我們將解釋 RxSwift 的基礎知識,當然,它們也是 Rx 世界的基礎知識。然後在 第二部分 中,我們將憑藉 MVVM 使用 RxSwift 建立一個專案。

響應式程式設計:

既然 RxSwift 是基於響應式程式設計的,那這究竟是什麼意思呢?

在計算機中,響應式程式設計或反應式程式設計(Reactive programming)是一種面向資料流和變化傳播的程式設計正規化。這意味著可以在程式語言中很方便地表達靜態或動態的資料流,而相關的計算模型會自動將變化的值通過資料流進行傳播。—— 維基百科

也許你在讀完後對本段的任何內容還是不怎麼了解,那下面我們就通過以下的例子來進一步理解它:

假設你現在有三個變數(a,b,c):

var a: Int = 1
var b: Int = 2
var c: Int = a + b // 輸出 3
複製程式碼

現在如果我們將 a 從 1 改為 2 並且我們列印 c,它的值仍然是 3。但是在響應式程式設計的世界中一切都變得不一樣了,c 的值取決於 ab,這意味著如果你把 a 從 1 改為 2,那 c 的值就會自動從 3 變為 4 而不需要你自行更改。

var a: Int = 1
var b: Int = 2
var c: Int = a + b // 輸出 3
a = 2
print("c=\(c)")
// 輸出 c=3
// 在響應式程式設計中 c=4
複製程式碼

現在讓我們開始學習 RxSwift 的基礎知識:

在 RxSwift(當然還有其他 Rx)的世界中,一切事物都是事件流,其中包括 UI 事件和網路請求等等。請切記這一點,我將用現實生活中的例子來解釋:

你的手機是一個 可觀察物件(Observable),它會產生一些事件,例如鈴聲或者推送通知等,這會讓你引起注意,事實上你訂閱(subscribe)了你的手機,並決定如何處理這些事件,比如你有時候刪除或者檢視一些通知,事實上這些事件是一些 訊號(signal),而你是做出決定的 觀察者(Observer)

[譯] iOS 裡的 MVVM 和 RxSwift

下面讓我們來程式碼來實現它:

Observable 和 Observer(訂閱者):

在 Rx 世界中,一些變數是 Observable,而另一些是 Observer(或訂閱者)。

因此 Observable 是通用的,如果它確遵循了 ObservableType 協議,你可以監聽你想要的任何型別。

[譯] iOS 裡的 MVVM 和 RxSwift

現在讓我們定義一些 Observable:

let helloObservableString = Observable.just("Hello Rx World")
let observableInt = Observable.of(0, 1, 2)
let dictSequence = Observable.from([1: "Hello", 2: "World"])
複製程式碼

在上面例子中,我們分別定義了 Observable 型別的 String,Int 和 Dictionary,現在我們應該 訂閱 我們的 Observable,這樣我們就可以從發出的訊號中讀取資訊。

helloObservableString.subscribe({ event in
    print(event)
})
// 輸出:
// next(Hello Rx World)
// completed
複製程式碼

你可能在想輸出為什麼會出現 nextcompleted,為什麼 ‘hello world’ 就不能好好列印一個字串,我得說這就是 Observable 最重要的特性:

實際上每個 Observable 都是一個 序列,與 Swift 裡 Sequence 的主要區別在於 Observable 的值可以是非同步的。如果你不理解這點並不重要,但是希望你能理解下面的描述,我以圖的方式呈現了這一特性:

sequence of events

在上面的圖中,我們有三個 Observable,第一行是 Int 型別,從 1 數到 6。在第二行是 String,從 ‘a’ 到 ‘f’,隨即發生了一些錯誤。最後一行是 Observable 型別的手勢,它還沒有完成,還在繼續。

這些顯示 Observable 變數事件的影像叫做大理石圖。想要了解更多資訊,您可以訪問 這個網站 或從 App Store 下載 這個 App(它也是開源的 ??,這裡 有 App 的原始碼)。

在 Rx 世界中,對於每個 Observable,都是由 3 種可能的列舉值組成:


  1. .next(value: T)

  2. .error(error: Error)

  3. .completed

當 Observable 新增值時,呼叫 next 並通過相關的 value 屬性(1 到 6,‘a’ 到 ‘f’)將值傳遞給 Observer(或訂閱者)。

如果 Observable 遇到了錯誤❌,則發出錯誤事件然後完成(‘f’ 之後的 X)。

如果 Observable 完成,則呼叫 completed 事件(6 之後)。

如果你想要取消訂閱一個 Observable,我們可以呼叫 dispose 方法,或者如果你想在你的 View deinit 的時候呼叫這個方法你應該使用 DisposeBag 在你的類反初始化時來進行你想要的操作。在這裡強調一點,如果你忘記使用 dispose 的話會導致記憶體洩漏☠️?。例如,你應該這樣訂閱 Observable:

let disposeBag = DisposeBag()
helloObservableString.subscribe({ event in
    print(event)
}).disposed(by: disposeBag)
複製程式碼

現在讓我們看看將 Rx 與函數語言程式設計相結合有多完美。假設你有 Observable 的 Int 並且你訂閱了它,現在 Observable 會給你一堆 Int,你可以使用很多方法改變來自 Observable 的發出訊號,例如:


Map:

你可以使用 map 方法讓訊號在到達訂閱者之前做出一些改變。例如,我們有 Observable 的 Int,它發出了 2,3,4 三個數字,現在我們想要它們在傳給訂閱者之前都乘以 10,我們可以這麼做:

map marble

Observable<Int>.of(2, 3, 4).map { value in
        return value * 10
    }.subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)
// 輸出:20 30 40
複製程式碼

Filter:

您可能又會想是否能讓它們在傳給訂閱者之前過濾掉一些值,例如,過濾掉示例中大於 25 的數字:

Observable<Int>.of(2, 3, 4).map { value in
        return value * 10
    }
    .filter( return $0 > 25 )
    .subscribe(onNext: {
        print($0)
    }).disposed(by: disposeBag)
// 輸出:30 40
複製程式碼

FlatMap:

又比如您有兩個 Observable 物件,並且您希望將它們合二為一:

[譯] iOS 裡的 MVVM 和 RxSwift

在上面的例子中,Observable A 和 Observable B 被組合在一起並形成一個新的 Observable:

let sequenceA = Observable<Int>.of(1, 2)
let sequenceB = Observable<Int>.of(1, 2)
let sequenceOfAB = Observable.of(sequenceA, sequenceB)
sequenceOfAB.flatMap { return $0 }.subscribe(onNext: {
    print($0)
}).disposed(by: disposeBag)
複製程式碼

distinctUntilChanged 或 debounce:

這兩個方法是搜尋中最有用的方法之一。例如,使用者想要搜尋單詞,您可能在使用者輸入每個字元時都呼叫搜尋 API。如果使用者快速鍵入,這樣的話你就會進行很多不必要的請求。為了達到此目的,正確的方法應該是在使用者停止鍵入時呼叫搜尋 API。這時您可以使用 debounce:

usernameOutlet.rx.text
    .debounce(0.3, scheduler: MainScheduler.instance)
    .subscribe(onNext: { [unowned self] text in
        self?.search(withQuery: text)
    }).addDisposableTo(disposeBag)
複製程式碼

在上面的例子中,如果使用者名稱 TextField 的內容在 0.3 秒內發生變化,則這些訊號不會到達訂閱者,因此不會呼叫搜尋方法。只有當使用者在 0.3 秒後停止輸入,訂閱者才會收到訊號並呼叫搜尋方法。

distinctUntilChanged 功能對變化很敏感,這意味著如果兩個訊號在訊號沒有變化之前得到相同的訊號,它將不會被髮送給使用者。


Rx 世界比你想象的要大得多,我在 文章的第二部分 中講述了我認為需要的一些基本概念,裡面有一個使用 RxSwift 的實際專案。

來自 raywenderlich 的 RxSwift 入門文章非常棒,我強烈推薦閱讀。

你可能不會在文章中注意到 RxSwift,因為它是 Swift 的高階概念之一,你可能每天都要閱讀不同的文章才能弄明白它。您可以通過 此連結 看到 RxSwift 部分中的幾篇好文章。

你可以通過 文章的第二部分 將 Rx 引入到 MVVM 的實際專案中,因為通過例項你將更好、更容易地理解 RxSwift 的概念。

你可以通過 Twitter 或者傳送 email 來聯絡到本文作者,郵箱是 mohammad_z74@icloud.com ✌️

如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章