MVVM 和 響應式編程入門

MVVM 和 響應式編程入門

架構的思考

簡要歸納一下 App 作的事?
架構是幹什麼的?git

App 的本質是反饋迴路

一個項目,最重要的兩個角色:Model 和 View。github

Model 決定是什麼(數據),View 決定如何展現,其餘的內容,基本都是處理二者的交互關係,以及提供這二者的服務。
若是要將 MV(X),其實基本討論都逃不過二者間的交互:算法

反饋迴路:編程

App 的任務

基於上圖呢,咱們的大部分的工做其實能夠拆分到下面 5 項中的一項:swift

  1. 構建:誰負責構建 model 和 view,以及將二者鏈接起來?
  2. 更新 model:如何處理 view action?
  3. 改變 View:如何將 model 的數據應用到 view 上去?
  4. view state:如何處理導航和其餘一些 model state(按鈕狀態、switch)
  5. 測試:爲了達到必定程度的測試覆蓋,要採起怎樣的測試策略

對於上面5個問題的回答,構成了 App 設計模式的基礎要件設計模式

view state 想一想頁面跳轉的邏輯,按鈕是否可點擊的狀態網絡

回顧 MVC

MVC 其實在上面的反饋迴路中間插入了 Controller,使得每條線都通過了 Controller。多線程

MVC 模塊職責

  1. 構建:誰負責構建 model 和 view,以及將二者鏈接起來?
  2. 更新 model:如何處理 view action?
  3. 改變 View:如何將 model 的數據應用到 view 上去?(單向數據流)

單向數據流:好比更改了用戶姓名,其實應該是隻負責更新 model 也就是 name 字段,而後經過 kvo,讓響應的nameLabel 改變顯示閉包

  1. view state:如何處理導航和其餘一些 model state
  2. 測試:爲了達到必定程度的測試覆蓋,要採起怎樣的測試策略(集成測試)

MVC 總結

最簡單的模式,適用於絕大部分狀況。

兩個地方不太盡人意:

  1. 觀察者模式失效

舉個例子:這段代碼有什麼潛在的問題?

func changeName(name : String) {
    person.name = name
    namelabel.text = name
}
  1. 肥大的 View Controller

責任重大,因此代碼量很是容易就動輒幾千行

對於這兩個問題,其實也有不少的解決辦法,第一個好比單向數據流,嚴格執行觀察者模式;第二個辦法很是很是多,好比 catogory,代理,代碼拆分。(objc中國、唐巧)

MVC 是萬能的嗎?

爲何還要學習下其餘的架構?

  • 借鑑思想彌補 MVC
  • 定義好框架嚴格執行
  • 鍛鍊設計、抽象的能力
  • 拓寬思惟
    。。。

MVVM - C

Model - View - ViewModel + 協調器(Coordinator)

從 MVC 到 MVVM

用過 MVVM的話 以爲這張圖有什麼問題嗎?

注意幾點:

  1. 必須建立 View-model。
  2. 必須創建起 View-model 和 View 之間的綁定。
  3. Model 由 View-model 擁有,而不是由 controller 擁有。

學習一下代碼,看是怎麼跑起來的

先演示一下 Demo,並看一下 MVVM - C 個模塊職責

協調器:

  1. 負責將 Model 的初始值,賦值給 rootViewController
  2. 全部和頁面跳轉的相關邏輯,(同時提供了新頁面所須要的數據)

ViewModel

  1. 持有 model
  2. 擁有一系列可觀察的信號量(序列)
  3. 提供數據的更新方法
  4. 私有輔助方法

ViewController & View

  1. 綁定 ViewModel的序列,和 View 的某個字段
  2. 將一些 ViewAction 調用的方法指向 ViewModel 中的數據更新方法
  3. 保存 View State
  4. 維護 View 的層級(demo中是 Storyboard)
  5. View 如何展現 (如何)

返回迴路,已經基本被摘出去了

Model

仍是那個 model

這樣咱們有個一個完整的管道

  1. 協調器協調了跳轉以及爲每一個要跳轉的 ViewController 的 ViewModel 設置初始的 model。
  2. ViewModel 使用 model提供直接可以使用的可觀察序列。(什麼是觀察序列?
  3. 爲了能直接使用,須要幹兩間事:合併序列以及數據變形。(什麼叫能直接使用?
  4. Controller 使用 bind 將準備好的值綁定到各個 View 上去。

來看反饋迴路:

  1. TableView 發送 Action
tableView.rx.modelDeleted(Item.self)
            .subscribe(onNext: { [unowned self] in self.viewModel.deleteItem($0) }).disposed(by: disposeBag)
  1. Controller 調用 ViewModel 的方法,來刪除數據
func deleteItem(_ item: Item) {
    folder.value.remove(item)
}
  1. 調用持久化層的 save 更改數據,產生一個通知
NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo)
  1. 通知早已經做爲一個序列,已經被其餘序列合併,形成多個序列的更新事件。
var folderContents: Observable<[AnimatableSectionModel<Int, Item>]> {
    return folderUntilDeleted.map { folder in
        guard let f = folder else {
            return [AnimatableSectionModel(model: 0, items: [])]
        }
        return [AnimatableSectionModel(model: 0, items: f.contents)]
    }
}

注意,和通知序列相關的好多序列都會收到更新,從而更新各類 View 的顯示或者狀態。

  1. 經過以前的 bind 更新 view 的顯示
viewModel.folderContents.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)

怎麼就綁定了?

先來看下函數響應式編程

函數響應式編程

先來看個例子

一個擁有用戶名和密碼輸入框的登陸界面:

產品經理說了需求:4句話:

  1. 用戶名不足 5 個字符的時候,給出紅色提示語1;
  2. 用戶名不足 5 個字符的時候,沒法輸入密碼,>=5時,能夠輸入
  3. 密碼不足 5 個時候,也顯示紅色提示語2;
  4. 用戶名和密碼有一個不符合要求時,底部綠色按鈕不可點擊,只有當用戶名和密碼同時有效時按鈕才能夠點擊。

通常的思路:

監聽 Username 輸入框,根據字符個數要考慮下面3件事:

  1. 提示語1是否顯示
  2. Password 是否可輸入
  3. 結合 Password 的情況,判斷按鈕是否能夠點擊

監聽 Password 輸入框,根據字符個數考慮下面2件事:

  1. 提示語2是否顯示
  2. 結合 UserName 的情況,判斷按鈕是否能夠點擊

因此開發過程:

有什麼問題?

  1. 須要翻譯這個過程
  2. 不少變化須要結合到一塊兒考慮,若是變化因素更多了,很容易出 bug。

其實這個翻譯過程,咱們本身把一些有聯繫的因素給放到一塊兒處理了,好比按鈕是均可點擊,須要同時監聽兩個文本框的狀態。

咱們可否只羅列條件,而後把這些條件扔給一個條件處理的機制,這個機制就能幫咱們正確的處理這些關係?

函數響應式編程來了:

咱們作兩件事情:

  1. 將條件做爲對象(序列)
  2. 將條件和結果進行綁定

開發過程變成了:

來看看代碼:

let usernameValid = usernameOutlet.rx.text.orEmpty
    .map { $0.characters.count >= minimalUsernameLength }
    .share(replay: 1) // without this map would be executed once for each binding, rx is stateless by default

let passwordValid = passwordOutlet.rx.text.orEmpty
    .map { $0.characters.count >= minimalPasswordLength }
    .share(replay: 1)

let everythingValid = Observable.combineLatest(usernameValid, passwordValid) { $0 && $1 }
    .share(replay: 1)

usernameValid
    .bind(to: passwordOutlet.rx.isEnabled)
    .disposed(by: disposeBag)

usernameValid
    .bind(to: usernameValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

passwordValid
    .bind(to: passwordValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

everythingValid
    .bind(to: doSomethingOutlet.rx.isEnabled)
    .disposed(by: disposeBag)

doSomethingOutlet.rx.tap
    .subscribe(onNext: { [weak self] _ in self?.showAlert() })
    .disposed(by: disposeBag)

無需翻譯,只須要羅列條件,接下來就是見證奇蹟的時刻

直觀的來看代碼清晰不少,而後不怎麼用動腦子,咱們下面來看看什麼是函數響應式編程再來講明他有哪些優缺點。

函數式編程

函數式編程是種編程範式,它須要咱們將函數做爲參數傳遞,或者做爲返回值返還。咱們能夠經過組合不一樣的函數來獲得想要的結果。

經過函數這個「管道」,數據從一頭通過「管道」到另外一頭,就獲得了想要的數據。

編程範式?(命令式、聲明式、函數式)http://www.javashuo.com/article/p-driicrqp-gb.html

函數響應式編程

函數式編程 + 響應

經過函數構建數據序列,最後經過適當的方式來響應這個序列,就是函數響應式編程。

在 Swift 中,咱們是用 RxSwift 來實現函數響應式編程!

RxSwift 核心

核心角色有如下5個

  • Observable - 可被監聽的序列 - 產生事件
  • Observer - 觀察者 - 響應事件
  • Operator - 操做符 - 建立變化組合事件
  • Disposable - 可被清楚的資源 - 管理綁定(訂閱)的生命週期
  • Schedulers - 調度器 - 線程隊列調配

以下圖所示:

Observable 可被監聽的序列(下面都簡稱序列)

一個序列,隨着時間的流逝,這個隊列將陸續產生一些能夠被觀察的值。

你能夠將溫度看做是一個序列,而後監測這個序列產生的值,最後對這個值作出響應。例如:當室溫高於 33 度時,打開空調降溫。

函數響應式編程裏最重要的就是構造序列。在函數響應式編程中,一切均可以看做是序列。

  1. 一次點擊事件
  2. 一個屬性的變化
  3. 一個網絡請求回調
  4. 。。。。

其實對於值的變化的隊列,好理解,那麼一次操做,或者一次網絡請求任務也看作是隊列,這個怎麼實現的?

大多數序列能夠產生3種事件:

public enum Event<Element> {
    case next(Element)
    case error(Swift.Error)
    case completed
}
  • next - 序列產生了一個新的元素
  • error - 建立序列時產生了一個錯誤,致使序列終止
  • completed - 序列的全部元素都已經成功產生,整個序列已經完成

因此當你想任何東西封裝成一個序列,只要 create 一個序列,而後在原有邏輯基礎上在適當的時機調用這些事件便可,而且 RxSwift 已經幫咱們建立了大量的序列:

button 的點擊
textField 的當前文本
switch 的開關狀態
Notification 隊列

若是本身建立,就須要調用上面說的事件了,好比咱們手動建立一個序列:

let numbers: Observable<Int> = Observable.create { observer -> Disposable in

    observer.onNext(0)
    observer.onNext(1)
    observer.onNext(2)
    observer.onNext(3)
    observer.onNext(4)
    observer.onNext(5)
    observer.onNext(6)
    observer.onNext(7)
    observer.onNext(8)
    observer.onNext(9)
    observer.onCompleted() // 結束

    return Disposables.create()
}

除了普通的隊列,框架還提供了好多其餘的隊列提供了不一樣的特性。

  • Single:它要麼只能發出一個元素,要麼產生一個 error 事件。
  • Completable :它要麼只能產生一個 completed 事件,要麼產生一個 error 事件。
  • Maybe:它介於 Single 和 Completable 之間,它要麼只能發出一個元素,要麼產生一個 completed 事件,要麼產生一個 error 事件。
  • 還有 Driver、ControlEvent

一個序列,其實就是一個被觀察者

觀察者

觀察者是:觀察序列的,響應序列事件的角色

建立一個觀察者:

tap.subscribe(onNext: { [weak self] in
    self?.showAlert()
}, onError: { error in
    print("發生錯誤: \(error.localizedDescription)")
}, onCompleted: {
    print("任務完成")
})

建立觀察者最直接的方法就是在 Observable 的 subscribe 方法後面描述,事件發生時,須要如何作出響應。而觀察者就是由後面的 onNext,onError,onCompleted的這些閉包構建出來的。

一樣框架爲咱們提供了不少觀察者,幾乎每一個類的全部屬性(包括自定義類),class.rx.xx 均可以做爲觀察者。

viewModel.navigationTitle.bind(to: rx.title).disposed(by: disposeBag)
viewModel.noRecording.bind(to: activeItemElements.rx.isHidden).disposed(by: disposeBag)
viewModel.hasRecording.bind(to: noRecordingLabel.rx.isHidden).disposed(by: disposeBag)
viewModel.timeLabelText.bind(to: progressLabel.rx.text).disposed(by: disposeBag)
viewModel.durationLabelText.bind(to: durationLabel.rx.text).disposed(by: disposeBag)
viewModel.sliderDuration.bind(to: progressSlider.rx.maximumValue).disposed(by: disposeBag)
viewModel.sliderProgress.bind(to: progressSlider.rx.value).disposed(by: disposeBag)
viewModel.playButtonTitle.bind(to: playButton.rx.title(for: .normal)).disposed(by: disposeBag)
viewModel.nameText.bind(to: nameTextField.rx.text).disposed(by: disposeBag)

Binder

觀察者有兩種:咱們此次只講下 Binder

  • AnyObserver
  • Binder

Binder 主要有如下兩個特徵:

  • 不會處理錯誤事件
  • 確保綁定都是在給定 Scheduler 上執行(默認 MainScheduler)

因此不少UI 觀察者都使用 Binder 去實現,只處理 Next ,而且在 主線程響應。

usernameValidOutlet.rx.isHidden 的由來

因爲頁面是否隱藏是一個經常使用的觀察者,因此應該讓全部的 UIView 都提供這種觀察者:

extension Reactive where Base: UIView {
  public var isHidden: Binder<Bool> {
      return Binder(self.base) { view, hidden in
          view.isHidden = hidden
      }
  }
}
usernameValid
    .bind(to: usernameValidOutlet.rx.isHidden)
    .disposed(by: disposeBag)

這樣你沒必要爲每一個 UI 控件單首創建該觀察者。這就是 usernameValidOutlet.rx.isHidden 的由來,許多 UI 觀察者 都是這樣建立的。

操做符

有了序列(被觀察者 Observable)和觀察者 (Observer),還差一點什麼?

我有了一個時間戳的序列,怎麼和觀察者(birthdayLabel.rx.text)綁定?
我有了 Username 和 Password 是否有效的序列,怎麼和 Button 是否能夠點擊的觀察者(doSomethingOutlet.rx.isEnabled)綁定?

序列須要變形、合併、相互影響!

操做符能夠幫助你們建立新的序列,或者變化組合原有的序列,從而生成一個新的序列。

https://beeth0ven.github.io/RxSwift-Chinese-Documentation/content/decision_tree.html

這裏只介紹一種最簡單經常使用的操做符,map,知道操做符的含義便可。

let usernameValid = usernameOutlet.rx.text.orEmpty
    .map { $0.characters.count >= minimalUsernameLength }
    .share(replay: 1)

Disposable

既然之後綁定和訂閱,確定有取消綁定和訂閱,怎麼取消呢,就用到了 Disposable。

最經常使用的是清除包(DisposeBag) 或者 takeUntil 操做符 來管理訂閱的生命週期。

var disposeBag = DisposeBag() // 來自父類 ViewController

override func viewDidLoad() {
    super.viewDidLoad()
    
    ...
    
    usernameValid
        .bind(to: passwordOutlet.rx.isEnabled)
        .disposed(by: disposeBag)
}

這個例子中 disposeBag 和 ViewController 具備相同的生命週期。當退出頁面時, ViewController 就被釋放,disposeBag 也跟着被釋放了,那麼這裏的綁定(訂閱)也就被取消了。這正是咱們所須要的。

調度器

Schedulers 是 Rx 實現多線程的核心模塊,它主要用於控制任務在哪一個線程或隊列運行。

let rxData: Observable<Data> = ...

rxData
    // 序列的構建函數在後臺運行
    .subscribeOn(ConcurrentDispatchQueueScheduler(qos: .userInitiated))
    // 主線程監聽和處理結果
    .observeOn(MainScheduler.instance)
    .subscribe(onNext: { [weak self] data in
        self?.data = data
    })
    .disposed(by: disposeBag)

函數響應式編程總結

我的感悟:

序列、操做符、觀察者分別被封裝成,交互過程當中的3個對象,將這3者進行了解耦。能夠靈活的組合,對序列進行變形、合併生成新的可直接使用的序列。提供一種更爲高效的編程方法,從數據變形的角度看是一種降維,因此這方面的 Bug 會少一些。同時,學習曲線過於陡峭,用很差極可能寫出很反人類的代碼,讓別人根本無法讀,另外使用綁定機制,出現一些 Bug 確實不易調試。

可是最難得的是這種思想,咱們還能夠把函數看作是對象,能夠做爲參數傳遞,能夠做爲返回值,就像一些設計模式,將算法封裝成對象,有了策略模式;將命令封裝成對象,有了命令模式;將狀態封裝成對象,有了狀態模式,都解決了一些領域裏的問題。學習這種思想,讓咱們之後在編碼上可以已更加寬廣的視角去看待問題。

回頭來看 MVVM 中的綁定

init(initialFolder: Folder = Store.shared.rootFolder) {
    folder = Variable(initialFolder)
    folderUntilDeleted = folder.asObservable()
        // Every time the folder changes
        .flatMapLatest { currentFolder in
            // Start by emitting the initial value
            Observable.just(currentFolder)
                // Re-emit the folder every time a non-delete change occurs
                .concat(currentFolder.changeObservable.map { _ in currentFolder })
                // Stop when a delete occurs
                .takeUntil(currentFolder.deletedObservable)
                // After a delete, set the current folder back to `nil`
                .concat(Observable.just(nil))
        }.share(replay: 1)
}

folder.asObservable()

對一個屬性生成一個序列的方式

flatMapLatest

「在數據源每次發出一個值的時候,它使用該值構建,開始,或者選擇一個新的可觀察量。不過這個變形可讓>咱們基於第一個可觀察量發出的狀態,來訂閱第二個可觀察量。」

—— 摘錄來自: Chris Eidhof. 「App 架構。」 iBooks.

just

concat

讓兩個或者多個 Observables 按順序串聯起來

concat 操做符將多個 Observables 按順序串聯起來,當前一個 Observable 元素髮送完畢後,後一個 `Observable 才能夠開始發出元素。

concat 將等待前一個 Observable 產生完成事件後,纔對後一個 Observable 進行訂閱。若是後一個是「熱」 Observable ,在它前一個 Observable 產生完成事件前,所產生的元素將不會被髮送出來。

currentFolder.changeObservable

var changeObservable: Observable<()> {
    return NotificationCenter.default.rx.notification(Store.changedNotification).filter { [weak self] (note) -> Bool in
        guard let s = self else { return false }
        if let item = note.object as? Item, item == s, !(note.userInfo?[Item.changeReasonKey] as? String == Item.removed) {
            return true
        } else if let userInfo = note.userInfo, userInfo[Item.parentFolderKey] as? Folder == s {
            return true
        }
        return false
    }.map { _ in () }
}

每次收到通知,只有通過 filter 函數檢驗爲 true 的元素,纔會被放到序列中做爲新事件。

takeUntil

currentFolder.deletedObservable

同 currentFolder.changeObservable,這不過這個是和刪除有關的通知,纔會放到序列。

concat(Observable.just(nil))

nil 將會在 takeUtil 執行後發出,想一想爲啥?

share(replay: 1)

屢次綁定只執行一次操做序列

folderUntilDeleted

咱們將 folder 這個可觀察量,與其餘由 model 驅動的,可能影響咱們 view 的邏輯的可觀察量,進行了合併。獲得的結果是一個新的可觀察量 folderUntilDeleted,它會在底層文件夾對象發生變化時正確更新,而且在底層文件夾對象被從 store 中刪除時將本身設置爲 nil。

MVVM-C 總結

  1. 很大程度上解決了 MVC 的痛點
  2. 響應式編程雙刃劍,學習成本陡峭,可是用起來會很是爽,總體代碼甚至會減小,bug也會減小,調試變得困難。

較少響應式編程的 MVVM

  1. tableview的 代理
  2. 使用 Notification 和 KVO 代替響應式編程

經驗和教訓

即便咱們不使用 MVVM 他的一些思想咱們仍是能夠借鑑的。

  1. 引入中間層
  2. 協調器,解耦多個 Controller
  3. 數據變形是單獨能夠提出來的

引用:

RxSwift 中文文檔:https://beeth0ven.github.io/RxSwift-Chinese-Documentation/
《App 架構》
Interactive diagrams of Rx Observables : http://rxmarbles.com
菜鳥教程 swift 教程: https://www.runoob.com/swift/swift-tutorial.html

相關文章
相關標籤/搜索