目錄html
簡要歸納一下 App 作的事?
架構是幹什麼的?git
一個項目,最重要的兩個角色:Model 和 View。github
Model 決定是什麼(數據),View 決定如何展現,其餘的內容,基本都是處理二者的交互關係,以及提供這二者的服務。
若是要將 MV(X),其實基本討論都逃不過二者間的交互:算法
反饋迴路:編程
基於上圖呢,咱們的大部分的工做其實能夠拆分到下面 5 項中的一項:swift
對於上面5個問題的回答,構成了 App 設計模式的基礎要件設計模式
view state 想一想頁面跳轉的邏輯,按鈕是否可點擊的狀態網絡
MVC 其實在上面的反饋迴路中間插入了 Controller,使得每條線都通過了 Controller。多線程
單向數據流:好比更改了用戶姓名,其實應該是隻負責更新 model 也就是 name 字段,而後經過 kvo,讓響應的nameLabel 改變顯示閉包
最簡單的模式,適用於絕大部分狀況。
兩個地方不太盡人意:
舉個例子:這段代碼有什麼潛在的問題?
func changeName(name : String) { person.name = name namelabel.text = name }
責任重大,因此代碼量很是容易就動輒幾千行
對於這兩個問題,其實也有不少的解決辦法,第一個好比單向數據流,嚴格執行觀察者模式;第二個辦法很是很是多,好比 catogory,代理,代碼拆分。(objc中國、唐巧)
爲何還要學習下其餘的架構?
Model - View - ViewModel + 協調器(Coordinator)
用過 MVVM的話 以爲這張圖有什麼問題嗎?
注意幾點:
學習一下代碼,看是怎麼跑起來的
返回迴路,已經基本被摘出去了
仍是那個 model
tableView.rx.modelDeleted(Item.self) .subscribe(onNext: { [unowned self] in self.viewModel.deleteItem($0) }).disposed(by: disposeBag)
func deleteItem(_ item: Item) { folder.value.remove(item) }
NotificationCenter.default.post(name: Store.changedNotification, object: notifying, userInfo: userInfo)
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 的顯示或者狀態。
viewModel.folderContents.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
先來看下函數響應式編程
一個擁有用戶名和密碼輸入框的登陸界面:
產品經理說了需求:4句話:
通常的思路:
監聽 Username 輸入框,根據字符個數要考慮下面3件事:
監聽 Password 輸入框,根據字符個數考慮下面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 來實現函數響應式編程!
核心角色有如下5個
以下圖所示:
一個序列,隨着時間的流逝,這個隊列將陸續產生一些能夠被觀察的值。
你能夠將溫度看做是一個序列,而後監測這個序列產生的值,最後對這個值作出響應。例如:當室溫高於 33 度時,打開空調降溫。
函數響應式編程裏最重要的就是構造序列。在函數響應式編程中,一切均可以看做是序列。
其實對於值的變化的隊列,好理解,那麼一次操做,或者一次網絡請求任務也看作是隊列,這個怎麼實現的?
大多數序列能夠產生3種事件:
public enum Event<Element> { case next(Element) case error(Swift.Error) case 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() }
除了普通的隊列,框架還提供了好多其餘的隊列提供了不一樣的特性。
一個序列,其實就是一個被觀察者
觀察者是:觀察序列的,響應序列事件的角色
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 主要有如下兩個特徵:
因此不少UI 觀察者都使用 Binder 去實現,只處理 Next ,而且在 主線程響應。
因爲頁面是否隱藏是一個經常使用的觀察者,因此應該讓全部的 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。
最經常使用的是清除包(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 確實不易調試。
可是最難得的是這種思想,咱們還能夠把函數看作是對象,能夠做爲參數傳遞,能夠做爲返回值,就像一些設計模式,將算法封裝成對象,有了策略模式;將命令封裝成對象,有了命令模式;將狀態封裝成對象,有了狀態模式,都解決了一些領域裏的問題。學習這種思想,讓咱們之後在編碼上可以已更加寬廣的視角去看待問題。
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) }
對一個屬性生成一個序列的方式
「在數據源每次發出一個值的時候,它使用該值構建,開始,或者選擇一個新的可觀察量。不過這個變形可讓>咱們基於第一個可觀察量發出的狀態,來訂閱第二個可觀察量。」
—— 摘錄來自: Chris Eidhof. 「App 架構。」 iBooks.
讓兩個或者多個 Observables 按順序串聯起來
concat 操做符將多個 Observables
按順序串聯起來,當前一個 Observable
元素髮送完畢後,後一個 `Observable
才能夠開始發出元素。
concat 將等待前一個 Observable 產生完成事件後,纔對後一個 Observable 進行訂閱。若是後一個是「熱」 Observable ,在它前一個 Observable 產生完成事件前,所產生的元素將不會被髮送出來。
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 的元素,纔會被放到序列中做爲新事件。
同 currentFolder.changeObservable,這不過這個是和刪除有關的通知,纔會放到序列。
nil 將會在 takeUtil 執行後發出,想一想爲啥?
屢次綁定只執行一次操做序列
咱們將 folder 這個可觀察量,與其餘由 model 驅動的,可能影響咱們 view 的邏輯的可觀察量,進行了合併。獲得的結果是一個新的可觀察量 folderUntilDeleted,它會在底層文件夾對象發生變化時正確更新,而且在底層文件夾對象被從 store 中刪除時將本身設置爲 nil。
即便咱們不使用 MVVM 他的一些思想咱們仍是能夠借鑑的。
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