前段時間在RxSwift上做了一些實踐彻亲,Rx確實是一個強大的工具,但同時也是一把雙刃劍室叉,如果濫用的話反而會帶來副作用睹栖,本文就引入Rx模式之后如何更好的管理應(yīng)用的狀態(tài)和邏輯做了一些粗淺的總結(jié)硫惕。
本文篇幅較長茧痕,主要圍繞著狀態(tài)管理這一話題進行介紹,前兩個部分介紹了前端領(lǐng)域中React和Vue所采用的狀態(tài)管理模式及其在Swift中的實現(xiàn)恼除,最后介紹了另一種簡化的狀態(tài)管理方案踪旷。不會涉及復(fù)雜的Rx特性,閱讀前對Rx有一些基本的了解即可豁辉。
為什么狀態(tài)管理這么重要
一個復(fù)雜的頁面通常需要維護大量的變量來表示其運行期間的各種狀態(tài)令野,在MVVM中頁面大部分的狀態(tài)和邏輯都通過ViewModel來維護,在常見的寫法中ViewModel和視圖之間通常用Delegate
來通訊徽级,比如說在數(shù)據(jù)改變的時候通知視圖層更新UI等等:
在這種模式中气破,ViewModel的狀態(tài)更新之后需要我們調(diào)用Delegate手動通知視圖層。而在Rx中這一層關(guān)系被淡化了餐抢,由于Rx是響應(yīng)式的现使,設(shè)定好綁定關(guān)系后ViewModel只需要改變數(shù)據(jù)的值低匙,Rx會自動的通知每一個觀察者:
Rx為我們隱藏了通知視圖的過程,首先這樣的好處是明顯的:ViewModel可以更加專注于數(shù)據(jù)本身碳锈,不用再去管UI層的邏輯顽冶;但是濫用這個特性也會帶來麻煩,大量的可觀察變量和綁定操作會讓邏輯變得含糊不清售碳,修改一個變量的時候可能會導(dǎo)致一系列難以預(yù)料的連鎖反應(yīng)强重,這樣代碼反而會變得更加難以維護。
想要更好的過渡到響應(yīng)式編程贸人,一個統(tǒng)一的狀態(tài)管理方案是不可或缺的间景。在這一塊前端領(lǐng)域有不少成熟的實踐方案,Swift中也有一些開源庫對其進行了實現(xiàn)艺智,其中的思想我們可以先來參考一下拱燃。
下面的介紹中所涉及的示例代碼在:https://github.com/L-Zephyr/MyDemos/tree/master/RxStateDemo。
Redux - ReSwift
Redux
是Facebook所提出的基于Flux改良的一種狀態(tài)管理模式力惯,在Swift中有一個名為ReSwift的開源項目實現(xiàn)了這個模式碗誉。
雙向綁定和單向綁定
要理解Redux首先要明白Redux是為了解決什么問題而生的,Redux為應(yīng)用提供統(tǒng)一的狀態(tài)管理父晶,并實現(xiàn)了單向的數(shù)據(jù)流哮缺。所謂的單向綁定
和雙向綁定
所描述的都是視圖(View)和數(shù)據(jù)(Model)之間的關(guān)系:
比方說有一個展示消息的頁面,首先需要從網(wǎng)絡(luò)加載最新的消息甲喝,在MVC中我們可以這樣寫:
class NormalMessageViewController: UIViewController {
var msgList: [MsgItem] = [] // 數(shù)據(jù)源
// 網(wǎng)絡(luò)請求
func request() {
// 1. 開始請求前播放loading動畫
self.startLoading()
MessageProvider.request(.news) { (result) in
switch result {
case .success(let response):
if let list = try? response.map([MsgItem].self) {
// 2. 請求結(jié)束后更新model
self.msgList = list
}
case .failure(_):
break
}
// 3. model更新后同步更新UI
self.stopLoading()
self.tableView.reloadData()
}
}
// ...
}
還可以將不需要的消息從列表中刪除:
extension NormalMessageViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
if editingStyle == .delete {
// 1. 更新model
self.msgList.remove(at: indexPath.row)
// 2. 刷新UI
self.tableView.reloadData()
}
}
// ...
}
在request
方法中我們通過網(wǎng)絡(luò)請求修改了數(shù)據(jù)msgList
尝苇,一旦msgList
發(fā)生改變必須刷新UI,顯然視圖的狀態(tài)跟數(shù)據(jù)是同步的埠胖;在tableView上刪除消息時糠溜,視圖層直接對數(shù)據(jù)進行操作然后刷新UI。視圖層即會響應(yīng)數(shù)據(jù)改變的事件直撤,又會直接訪問和修改數(shù)據(jù)非竿,這就是一個雙向綁定的關(guān)系:
雖然在這個例子中看起來非常簡單,但是當頁面比較復(fù)雜的時候UI操作和數(shù)據(jù)操作混雜在一起會讓邏輯變得混亂谋竖『熘看到這里單向綁定
的含義就很明顯了,它去掉了View -> Model
的這一層關(guān)系蓖乘,視圖層不能直接對數(shù)據(jù)進行修改锤悄,它只能通過某種機制向數(shù)據(jù)層傳遞事件,并在數(shù)據(jù)改變的時候刷新UI嘉抒。
實現(xiàn)
為了構(gòu)造單向數(shù)據(jù)流零聚,Redux引入了一系列概念,這是Redux中所描述的數(shù)據(jù)流:
其中的State
就是應(yīng)用的狀態(tài),也就是我們的Model部分隶症,先不管這里的Action
容诬、Reducer
等概念,從圖中可以看到State和View是有著直接的綁定關(guān)系的沿腰,而View的事件則會通過Action
览徒、Store
等一系列操作間接的改變State
,下面來詳細的介紹一下Redux的數(shù)據(jù)流的實現(xiàn)以及所涉及到的概念:
View
顧名思義颂龙,View就是視圖习蓬,用戶在視圖上的操作事件不會直接修改模型,而是會被映射成一個個Action
措嵌。-
Action
Action
表示一個對數(shù)據(jù)操作的請求躲叼,Action會被發(fā)送到Store
中,這是對模型數(shù)據(jù)進行修改的唯一辦法企巢。在ReSwift中有一個名為Action的協(xié)議(僅作標記用的空協(xié)議)枫慷,對于Model中數(shù)據(jù)的每個操作,比如說設(shè)置一個值浪规,都需要有一個對應(yīng)的Action:
/// 設(shè)置數(shù)據(jù)的Action struct ActionSetMessage: Action { var news: [MsgItem] = [] } /// 移除某項數(shù)據(jù)的Action struct ActionRemoveMessage: Action { var index: Int }
用
struct
類型來表示一個Action或听,Action所攜帶的數(shù)據(jù)保存在其成員變量中。 -
Store和State
就像上面所提到的笋婿,State
表示了應(yīng)用中的Model數(shù)據(jù)誉裆,而Store
則是存放State的地方;在Redux中Store是一個全局的容器缸濒,所有組件的狀態(tài)都被保存在里面足丢;Store接受一個Action,然后修改數(shù)據(jù)并通知視圖層更新UI庇配。如下所示斩跌,每一個頁面和組件都有各自的狀態(tài)以及用來儲存狀態(tài)的Store:
// State struct ReduxMessageState: StateType { var newsList: [MsgItem] = [] } // Store,直接使用ReSwift的Store類型來初始化即可捞慌,初始化時要指定reducer和狀態(tài)的初始值 let newsStore = Store<ReduxMessageState>(reducer: reduxMessageReducer, state: nil)
Store
通過一個dispatch
方法來接收Action
耀鸦,視圖調(diào)用這個方法來向Store傳遞Action:messageStore.dispatch(ActionRemoveMessage(index: 0))
-
Reducer
Reducer
是一個比較特殊的函數(shù),這里其實是借鑒了函數(shù)式的一些思想卿闹,首先Redux強調(diào)了數(shù)據(jù)的不可變性(Immutable)揭糕,簡單來說就是一個數(shù)據(jù)模型在創(chuàng)建之后就不可被修改萝快,那當我們要修改Model某個屬性時要怎么辦呢锻霎?答案就是創(chuàng)建一個新的Model,Reducer
的作用就體現(xiàn)在這里:Reducer
是一個函數(shù)揪漩,它的簽名如下:(_ action: Action, _ state: StateType?) -> StateType
接受一個表示動作的action和一個表示當前狀態(tài)的state旋恼,然后計算并返回一個新的State,隨后這個新的State會被更新到Store中:
// Store.swift中的實現(xiàn) open func _defaultDispatch(action: Action) { guard !isDispatching else { raiseFatalError("...") } isDispatching = true let newState = reducer(action, state) // 1. 通過reducer計算出新的state isDispatching = false state = newState // 2. 直接將新的state賦值到當前的state上 }
應(yīng)用中所有數(shù)據(jù)模型的更新操作最終都通過
Reducer
來完成奄容,為了保證這一套流程可以正常的完成冰更,Reducer
必須是一個純函數(shù):它的輸出只取決于輸入的參數(shù)产徊,不依賴任何外部變量,同樣也不能包含任何異步的操作蜀细。在這個例子中的
Reducer
是這樣寫的:func reduxMessageReducer(action: Action, state: ReduxMessageState?) -> ReduxMessageState { var state = state ?? ReduxMessageState() // 根據(jù)不同的Action對數(shù)據(jù)進行相應(yīng)的修改 switch action { case let setMessage as ActionSetMessage: // 設(shè)置列表數(shù)據(jù) state.newsList = setMessage.news case let remove as ActionRemoveMessage: // 移除某一項 state.newsList.remove(at: remove.index) default: break } // 最后直接返回修改后的整個State結(jié)構(gòu)體 return state }
最后在視圖中實現(xiàn)StoreSubscriber
協(xié)議接收State改變的通知并更新UI即可舟铜。詳細的代碼請看Demo中的Redux
文件夾。
分析
Redux將View -> Model
這一層關(guān)系分解成了View -> Action -> Store -> Model
奠衔,每一個模塊只負責(zé)一件事情谆刨,數(shù)據(jù)始終沿著這條鏈路單向傳遞。
-
優(yōu)點:
在處理大量狀態(tài)的時候單向數(shù)據(jù)流更加容易維護归斤,所有事件都通過唯一的入口
dispatch
手動觸發(fā)痊夭,數(shù)據(jù)的每一個處理過程都是透明的,這樣就可以追蹤到每一次的狀態(tài)變更操作脏里。在前端中Redux的配套工具redux-devtools就提供了一個名為Time Travel
的功能她我,能夠回溯應(yīng)用的任意歷史狀態(tài)。全局Store有利于在多個組件之間共享狀態(tài)迫横。
-
缺點:
首先Redux為它的數(shù)據(jù)流指定了大量的規(guī)則番舆,無疑會帶來更高的學(xué)習(xí)成本。
在Redux的核心模型中并沒有考慮異步(Reducer是純函數(shù))矾踱,所以如網(wǎng)絡(luò)請求這樣的異步任務(wù)還需要通過
ActionCreator
之類的機制間接處理合蔽,進一步提升了復(fù)雜度。-
另一個被廣為詬病的缺點是介返,Redux會引入大量樣板代碼拴事,在上面這個簡單的例子中我們需要為頁面創(chuàng)建Store、State圣蝎、Reducer刃宵、Action等不同的結(jié)構(gòu):
即便是修改一個狀態(tài)變量這樣簡單的操作都需要經(jīng)過這一套流程,這無疑會大大增加代碼量徘公。
綜上所述牲证,Redux模式雖然有許多優(yōu)點,但它帶來的成本也無法忽視关面。如果你的頁面和交互極其復(fù)雜或是多個頁面之間有大量的共享狀態(tài)的話可以考慮Redux坦袍,但是對于大部分應(yīng)用來說,Redux模式并不太適用等太。
Vuex - ReactorKit
Vue
也是近年來十分熱門的前端框架之一捂齐,Vuex
則是其專門為Vue
提出的狀態(tài)管理模式,在Redux之上進行了一些優(yōu)化缩抡;而ReactorKit
是一個Swift的開源庫奠宜,它的一些設(shè)計理念與Vuex十分相似,所以這里我將它們放在一起來講。
實現(xiàn)
與ReSwift
不同的是ReactorKit
的實現(xiàn)本身便于基于RxSwift
压真,所以不必再考慮如何與Rx結(jié)合娩嚼,下面是ReactorKit
中數(shù)據(jù)的流程圖:
大體流程與Redux類似,不同的是Store
變成了Reactor
滴肿,這是ReactorKit
引入的一個新概念岳悟,它不要求在全局范圍統(tǒng)一管理狀態(tài),而是每個組件管理各自的狀態(tài)泼差,所以每個視圖組件都有各自所對應(yīng)的Reactor
竿音。
具體的代碼請看Demo中的ReactorKit
文件夾,各個部分的含義如下:
-
Reactor:
現(xiàn)在用
ReactorKit
來重寫上面的那個例子拴驮,首先需要為這個頁面創(chuàng)建一個實現(xiàn)了Reactor
協(xié)議的類型MessageReactor
:class MessageReactor: Reactor { // 與Redux中的Action作用相同春瞬,可以是異步 enum Action { case request case removeItem(Int) } // 表示修改狀態(tài)的動作(同步) enum Mutation { case setMessageList([MsgItem]) case removeItem(Int) } // 狀態(tài) struct State { var newsList: [MsgItem] = [] } ... }
一個Reactor需要定義
State
、Action
套啤、Mutation
這三個部分宽气,后面會一一介紹。首先比起Redux這里多了一個
Mutation
的概念潜沦,在Redux中由于Action直接與Reducer中的操作對應(yīng)萄涯,所以Action只能用來表示同步的操作。ReactorKit
將這個概念更加細化唆鸡,拆分成了兩個部分:Action
和Mutation
:-
Action
:視圖層觸發(fā)的動作涝影,可以表示同步和異步(比如網(wǎng)絡(luò)請求),它最終會被轉(zhuǎn)換成Mutation再被傳遞到Reducer中争占; -
Mutation
:只能表示同步操作燃逻,相當于Redux模式中的Action,最終被傳入Reducer中參與新狀態(tài)的計算臂痕;
-
-
mutate():
mutate()
是Reactor中的一個方法伯襟,用來將用戶觸發(fā)的Action
轉(zhuǎn)換成Mutation
,mutate()
的存在使得Action可以表示異步操作握童,因為無論是異步還是同步的Action最后都會被轉(zhuǎn)換成同步的Mutation:func mutate(action: MessageReactor.Action) -> Observable<MessageReactor.Mutation> { switch action { case .request: // 1. 異步:網(wǎng)絡(luò)請求結(jié)束后將得到的數(shù)據(jù)轉(zhuǎn)換成Mutation return service.request().map { Mutation.setMessageList($0) } case .removeItem(let index): // 2. 同步:直接用just包裝一個Mutation return .just(Mutation.removeItem(index)) } }
值得一提的是姆怪,這里的
mutate()
方法返回的是一個Observable<Mutation>
類型的實例,得益于Rx強大的描述能力澡绩,我們可以用一致的方式來處理同步和異步代碼稽揭。 -
reduce():
reduce()
方法這里就沒太多可說的了,它扮演的角色與Redux中的Reducer一樣肥卡,唯一不同的是這里接受的是一個Mutation
類型溪掀,但本質(zhì)是一樣的:func reduce(state: MessageReactor.State, mutation: MessageReactor.Mutation) -> MessageReactor.State { var state = state switch mutation { case .setMessageList(let news): state.newsList = news case .removeItem(let index): state.newsList.remove(at: index) } return state }
-
Service
圖中還有一個與
mutate()
產(chǎn)生交互的Service
對象,Service指的是實現(xiàn)具體業(yè)務(wù)邏輯的地方召调,Reactor
會通過各個Service
對象來執(zhí)行具體的業(yè)務(wù)邏輯膨桥,比如說網(wǎng)絡(luò)請求:protocol MessageServiceType { /// 網(wǎng)絡(luò)請求 func request() -> Observable<[MsgItem]> } final class MessageService: MessageServiceType { func request() -> Observable<[MsgItem]> { return MessageProvider .rx .request(.news) .mapModel([MsgItem].self) .asObservable() } }
看到這里
Reactor
的本質(zhì)基本上已經(jīng)明了:Reactor
實際上是一個中間層蛮浑,它負責(zé)管理視圖的狀態(tài)唠叛,并作為視圖和具體業(yè)務(wù)邏輯之間通訊的橋梁只嚣。
此外ReactorKit
希望我們的所有代碼都通過函數(shù)響應(yīng)式(FRP)的風(fēng)格來編寫,這從它的API設(shè)計上可以看出:Reactor
類型中沒有提供如dispatch
這樣的方法艺沼,而是只提供了一個Subject
類型的變量action
:
var action: ActionSubject<Action> { get }
在Rx中Subject
既是觀察者又是可觀察對象册舞,常常扮演一個中間橋梁的角色。視圖上所有的Action
都通過Rx綁定到action
變量上障般,而不是通過手動觸發(fā)的方式:比方說我們想在viewDidLoad
的時候發(fā)起一個網(wǎng)絡(luò)請求调鲸,常規(guī)的寫法是這樣的:
override func viewDidLoad() {
super.viewDidLoad()
service.request() // 手動觸發(fā)一個網(wǎng)絡(luò)請求動作
}
而ReactorKit
所推崇的函數(shù)式風(fēng)格是這樣的:
// bind是統(tǒng)一進行事件綁定的地方
func bind(reactor: MessageReactor) {
self.rx.viewDidLoad // 1. 將viewDidLoad作為一個可觀察的事件
.map { Reactor.Action.request } // 2. 將viewDidLoad事件轉(zhuǎn)成Action
.bind(to: reactor.action) // 3. 綁定到action變量上
.disposed(by: self.disposeBag)
// ...
}
bind
方法是視圖層進行事件綁定的地方,我們將VC的viewDidLoad
作為一個事件源挽荡,將其轉(zhuǎn)換成網(wǎng)絡(luò)請求的Action之后綁定到reactor.action
上藐石,這樣當VC的viewDidLoad被調(diào)用時該事件源就會發(fā)出一個事件并觸發(fā)Reactor
中網(wǎng)絡(luò)請求的操作。
這樣的寫法是更加FRP定拟,一切都是事件流于微,但是實際用起來并不是那么完美。首先我們需要為用到的所有UI組件提供Rx擴展(上面的例子使用了RxViewController這個庫)青自;其次這對reactor實例初始化的時機有更加嚴格的要求株依,因為bind
方法是在reactor實例初始化的時候自動調(diào)用的,所以不能在viewDidLoad
中初始化延窜,否則會錯過viewDidLoad
事件恋腕。
分析
-
優(yōu)點:
- 相比ReSwift簡化了一些流程,并且以組件為單位來管理各自的狀態(tài)逆瑞,相比起來更容易在現(xiàn)有工程中引入荠藤;
- 與
RxSwfit
很好的結(jié)合在了一起,能提供較為完善的函數(shù)響應(yīng)式(FRP)開發(fā)體驗获高;
-
缺點:
- 因為核心思想還是Redux模式商源,所以模板代碼過多的問題還是無法避免;
另一種簡化方案
Redux模式對于大部分應(yīng)用來說還是過于沉重了谋减,而且Swift的語言特性也不像JavaScript那樣靈活牡彻,很多樣板代碼無法避免。所以這里總結(jié)了另一套簡化的方案出爹,希望能在享受單向數(shù)據(jù)流優(yōu)勢的同時減輕使用者的負擔庄吼。
詳細的代碼請看Demo中的Custom
文件夾:
實現(xiàn)非常簡單,核心是一個Store
類型:
public protocol StateType { }
public class Store<ConcreteState>: StoreType where ConcreteState: StateType {
public typealias State = ConcreteState
/// 狀態(tài)變量严就,一個只讀類型的變量
public private(set) var state: State
/// 狀態(tài)變量對應(yīng)的可觀察對象总寻,當狀態(tài)發(fā)生改變時`rxState`會發(fā)送相應(yīng)的事件
public var rxState: Observable<State> {
return _state.asObservable()
}
/// 強制更新狀態(tài),所有的觀察者都會收到next事件
public func forceUpdateState() {
_state.onNext(state)
}
/// 在一個閉包中更新狀態(tài)變量梢为,當閉包返回后一次性應(yīng)用所有的更新渐行,用于更新狀態(tài)變量
public func performStateUpdate(_ updater: (inout State) -> Void) {
updater(&self.state)
forceUpdateState()
}
...
}
其中StateType
是一個空協(xié)議嚼摩,僅作為類型約束用市殷;Store
作為一個基類,負責(zé)保存組件的狀態(tài),以及管理狀態(tài)更新的數(shù)據(jù)源柜候,核心代碼非常簡單柿祈,下面來看一下實際應(yīng)用仗岖。
ViewModel
在實際開發(fā)中我讓ViewModel
來處理狀態(tài)管理和變更的邏輯慰丛,再來實現(xiàn)一次上面的那個例子,將一個業(yè)務(wù)方的ViewModel
分成三個部分:
// <1>
struct MessageState: StateType {
...
}
// <2>
extension Reactive where Base: MessageViewModel {
...
}
// <3>
class MessageViewModel: Store<MessageState> {
required public init(state: MessageState) {
super.init(state: state)
}
...
}
各個部分的含義如下:
-
定義頁面的狀態(tài)變量
描述一個頁面所需的所有狀態(tài)變量都需要定義在一個單獨的實現(xiàn)了
StateType
協(xié)議的struct
中:struct MessageState: StateType { var msgList: [MsgItem] = [] // 原始數(shù)據(jù) }
從前面的代碼中可以看到
Store
中有一個只讀的state
屬性:public private(set) var state: State
業(yè)務(wù)方的ViewModel直接通過
self.state
來訪問當前的狀態(tài)變量套鹅。而修改狀態(tài)變量則通過一個performStateUpdate
方法來完成站蝠,方法簽名如下:public func performStateUpdate(_ updater: (inout State) -> Void)
ViewModel在修改狀態(tài)變量的時候通過
updater
閉包中的參數(shù)直接進行修改:performStateUpdate { $0.msgList = [...] } // 修改狀態(tài)變量
執(zhí)行完畢后頁面的狀態(tài)會被更新,所綁定的UI組件也會接受到狀態(tài)更新的事件卓鹿。這樣一來能避免為每一個狀態(tài)變量創(chuàng)建一個Action菱魔,簡化了流程,同時所有更新狀態(tài)的操作都由經(jīng)過同一個入口吟孙,有利于之后的分析澜倦。
統(tǒng)一管理狀態(tài)變量有以下幾個優(yōu)點:
- 邏輯清晰:在瀏覽頁面的代碼時只要查看這個類型就能知道哪些變量是需要特別關(guān)注的;
-
頁面持久化:只需序列化這個結(jié)構(gòu)體就能夠保存這個頁面的全部信息拔疚,在恢復(fù)時只需要將反序列化出來的State賦值給
ViewModel
的state
變量即可:self.state = localState
肥隆;
- 便于測試:單元測試時可以通過檢查State類型的變量來進行測試;
-
定義對外暴露的可觀察變量(Getter)
ViewModel需要暴露一些能讓視圖進行綁定的可觀察對象(Observable)稚失,
Store
中提供了一個名為rxState
的Observable<State>
類型對象作為狀態(tài)更新的統(tǒng)一事件源栋艳,但是為了更加便于視圖層使用,我們需要將其進一步細化句各。這部分邏輯定義在ViewModel的
Rx擴展
中吸占,對外提供可觀察的屬性,這里定義了視圖層需要綁定的所有狀態(tài)凿宾。這部分的作用相當于Getter
矾屯,是視圖層從ViewModel中獲取數(shù)據(jù)源的接口:extension Reactive where Base: MessageViewModel { var sections: Observable<[MessageTableSectionModel]> { return base .rxState // 從統(tǒng)一的事件源rxState中分流 .map({ (state) -> [MessageTableSectionModel] in // 將VM中的后端原始模型類型轉(zhuǎn)換成UI層可以直接使用的視圖模型 return [ MessageTableSectionModel(items: state.msgList.map { MessageTableCellModel.news($0) }) ] }) } }
這樣一來視圖層不需要關(guān)心
State
中的數(shù)據(jù)類型,直接通過rx
屬性來獲取自己需要觀察的屬性即可:// 視圖層直接觀察sections初厚,不需要關(guān)心內(nèi)部的轉(zhuǎn)換邏輯 vm.rx.sections.subscribe(...)
為什么要將視圖層使用的接口定義在擴展中件蚕,而不是直接觀察基類中的
rxState
:- 定義在Rx擴展中的變量可以直接通過ViewModel的rx屬性訪問到,便于視圖層使用产禾;
- State中的原始數(shù)據(jù)可能需要一定轉(zhuǎn)換才能讓視圖層使用(比如上面將原始的
MsgItem
類型轉(zhuǎn)換成TableView可以直接使用的SectionModel模型)排作,這部分的邏輯適合放在擴展的計算屬性中,讓視圖層更加純粹亚情;
-
對外提供的方法(Action)
ViewModel還需要接收視圖層的事件以觸發(fā)具體的業(yè)務(wù)邏輯妄痪,如果這一步通過Rx綁定的方式來完成的話,會對業(yè)務(wù)層代碼的編寫方式帶來很多限制(參考上面的ReactorKit)楞件。所以這部分不做過多的封裝衫生,還是通過方法的形式來對外暴露接口裳瘪,這部分就相當于Action,不過這樣的代價是Action無法再通過統(tǒng)一的接口來派發(fā):
class MessageViewModel: Store<MessageState> { // 請求 func request() { state.loadingState = .loading MessageProvider.rx .request(.news) .map([MsgItem].self) .subscribe(onSuccess: { (items) in // 請求完成后改變state中響應(yīng)的變量罪针,UI層會自動響應(yīng) self.performStateUpdate { $0.msgList = items $0.loadingState = .normal } }, onError: { error in self.performStateUpdate { $0.loadingState = .normal } }) .disposed(by: self.disposeBag) } }
我們之前已經(jīng)將狀態(tài)和UI完全分離開來了彭羹,所以在ViewModel的邏輯中只需要關(guān)心
state
中的狀態(tài)即可,不需要關(guān)心與視圖層的交互站故,所以以這種方式編寫的代碼同樣也是十分清晰的皆怕。
View
視圖層需要實現(xiàn)一個名為View
的協(xié)議毅舆,這里主要參考了ReactorKit
中的設(shè)計:
/// 視圖層協(xié)議
public protocol View: class {
/// 用于聲明該視圖對應(yīng)的ViewModel的類型
associatedtype ViewModel: StoreType
/// ViewModel的實例西篓,有默認實現(xiàn),視圖層需要在合適的時機初始化
var viewModel: ViewModel? { set get }
/// 視圖層實現(xiàn)這個方法憋活,并在其中進行綁定
func doBinding(_ vm: ViewModel)
}
對于視圖層來說岂津,它需要做兩件事:
-
實現(xiàn)一個
doBinding
方法,所有的Rx事件綁定都放在這個方法中完成:func doBinding(_ vm: MessageViewModel) { vm.rx.sections .drive(self.tableView.rx.items(dataSource: dataSource)) .disposed(by: self.disposeBag) }
-
在合適的時機初始化
viewModel
屬性:override func viewDidLoad() { super.viewDidLoad() // 初始化ViewModel self.viewModel = MessageViewModel(state: MessageState()) }
當
viewModel
初始化完成后會自動調(diào)用doBinding
方法進行綁定悦即,并且在實例的生命周期中只會被執(zhí)行一次吮成。
在視圖層中對于各種狀態(tài)的綁定是很重要的一個環(huán)節(jié),View
協(xié)議存在的意義在于將視圖層的事件綁定規(guī)范化辜梳,防止綁定操作的代碼散落在各處降低可讀性粱甫。
數(shù)據(jù)流
按照以上流程實現(xiàn)的頁面數(shù)據(jù)流如下:
- 視圖(View)中的事件觸發(fā)時,直接調(diào)用相應(yīng)的方法觸發(fā)
ViewModel
中的邏輯作瞄; -
ViewModel
中執(zhí)行具體的業(yè)務(wù)邏輯茶宵,并通過performStateUpdate
修改保存在State中的狀態(tài)變量; - 狀態(tài)變量發(fā)生改變之后宗挥,通過Rx的綁定自動通知視圖層更新UI乌庶;
這樣能保證一個頁面的數(shù)據(jù)始終按照預(yù)期的方式來變化,而且單向數(shù)據(jù)流的特點使得我們可以像Redux這樣追蹤所有狀態(tài)的變更契耿,比如說我們可以簡單的利用Swift的反射(Mirror
)來將所有狀態(tài)的變更打印到控制臺中:
public func performStateUpdate(_ updater: (inout State) -> Void) {
updater(&self.state)
#if DEBUG
StateChangeRecorder.shared.record(state, on: self) // 記錄狀態(tài)的變更
#endif
forceUpdateState()
}
實現(xiàn)的代碼在StateChangeRecorder.swift
文件中瞒大,非常簡單只有不到100行。每當有狀態(tài)發(fā)生改變的時候就會在控制臺中打印一條Log:
如果你為所有StateType
類型實現(xiàn)序列化和反序列化的操作搪桂,甚至可以實現(xiàn)類似redux-devtools這樣的Time Travel
功能透敌,這里就不再繼續(xù)引申了。
總結(jié)
引入Rx模式需要多方面的考慮踢械,本文僅針對狀態(tài)管理這一點作了介紹酗电,上面介紹的三種方案各有特點,最終的選擇還是要結(jié)合項目的實際情況來判斷裸燎。