簡介
最近接手了一個N年前的老項目,由于實在是過于陳舊,于是提出想要重構(gòu)項目該項目的想法炉峰,沒想到和項目經(jīng)理竟然達成了共識当悔。在重構(gòu)初始階段項目框架搭建時傅瞻,考慮到將來所承載的業(yè)務(wù)的可變性比較大,如果單純的從翻新項目的角度出發(fā)盲憎,可能會在日后的迭代過程中埋下隱藏的炸彈嗅骄,所以決定使用MVVM
架構(gòu)進行新項目的研發(fā)。項目基礎(chǔ)組件已經(jīng)完成開發(fā)饼疙,目前溺森,正在進行登錄業(yè)務(wù)模塊的功能開發(fā),通過使用Rx
讓MVVM
的實現(xiàn)不管是從可測試性還是可擴展性而言窑眯,都變得十分的優(yōu)雅了屏积。甚至對于業(yè)務(wù)的剝離的效果十分明顯。
接下來磅甩,我把到目前為止的所有實現(xiàn)分為以下3個核心部分:
???(1). MVVM
架構(gòu)思想炊林;
???(2). 函數(shù)響應式編程;
???(3). 登錄的實踐卷要;
MVVM架構(gòu)思想
分析下現(xiàn)在熱門的幾款架構(gòu)模式中:MVP
渣聚、MVCS
却妨、VIPER
以及MVVM
究其根本饵逐,其實都是沖MVC
衍變優(yōu)化而來。當然彪标,對于經(jīng)典的MVC
我們用一句話概括:盡管這種設(shè)計足夠優(yōu)秀倍权,但是在使用過程中很難確定究竟應該在哪個模塊做什么事情,以至于出現(xiàn)了Controller
變臃腫和胖Model
的窘境。
回過頭來看MVVM
薄声,現(xiàn)在可以說但凡是個程序猿当船,沒有不知道它的。不管你在項目中如何使用它默辨,都逃離不掉3大模塊:
???(1). 數(shù)據(jù)層德频,也就是Model
;
???(2). 視圖模型層缩幸,也就是ViewModel
壹置;
???(3). 視圖層,也就是View
或者ViewController
表谊;
???PS:有些會把它分為4塊钞护,我更傾向于ViewController
包含于視圖層的觀點。
正是由于在View
和Model
之間多加了一層ViewModel
爆办,讓原來無處安放的網(wǎng)絡(luò)請求和大量的復雜業(yè)務(wù)有了明確的去處难咕,解決了原來MVC
測試性差和日益臃腫的Controller
的問題。同樣將業(yè)務(wù)抽離到ViewModel
中后距辆,也降低了代碼的耦合度余佃,提高了復用性。因為對于同樣的業(yè)務(wù)跨算,如果采用在Controller
中實現(xiàn)的話爆土,當不同界面的Controller
要使用到相同的業(yè)務(wù)時,就沒辦法服用之前的代碼漂彤,而需要復制粘貼了雾消。但是有了ViewModel
后,同樣的業(yè)務(wù)挫望,只需在對應的Controller
中執(zhí)行同樣的ViewModel
就行了。
MVVM
的精髓我認為是在于如何處理View
和Model
綁定的問題狂窑。按照以前的慣例媳板,我們或許會在某一個cell
中聲明一個model
屬性,然后在實現(xiàn)文件中自定義它的setter
方法泉哈,最后拿到數(shù)據(jù)進行展示蛉幸。除非你的model
沒有做過翻譯直接是一個dictionary
,或者通過id
類型使用kvc
取值丛晦,否則這種方法會導致view
和model
形成跨層的訪問奕纫。從大大降低了代碼的可閱讀性,對于后期維護十分不便烫沙。
為了解決這樣的跨層訪問匹层,通常ViewModel
中可以通過kvo
來實現(xiàn)model
到view
的映射。但是當view
接收到用戶操作后需要進行model
修改時锌蓄,常用的就是Target-Action
或delegate
升筏,甚至于Notification
撑柔、block
也可以,這些方法并非不可取您访,也足夠優(yōu)秀铅忿,但是卻比較繁雜。而雙向綁定的的出現(xiàn)就完美且優(yōu)雅的改變了這種方式灵汪,這也是為什么一提到MVVM
我們就能連想到Rx
或RAC
的原因了檀训。
同樣,正是由于雙向綁定的出現(xiàn)享言,Controller
完成的職責就很明確了峻凫,即將ViewModel
和View
進行綁定。所以業(yè)內(nèi)流傳的這張圖片就很好的體現(xiàn)了這一變化:
經(jīng)過這樣的衍變之后担锤,不難看出MVVM
的關(guān)系基本就是:View
<-> C
<-> ViewModel
<-> Model
蔚晨。最終,促成了業(yè)務(wù)和UI
的分離肛循,規(guī)范了View
和Controller
強耦合的性質(zhì)铭腕。邏輯上而言,只有Controller
知道需要呈現(xiàn)的View
多糠,也只有它知道需要使用的業(yè)務(wù)ViewModel
累舷,兩個層級之間通過Controller
形成橋梁進行通信。當然這里有個問題是夹孔,當用戶操作通過View
想要改變Model
的屬性時被盈,業(yè)務(wù)線會變得比較長,而這塊將會在后面列表展示時體現(xiàn)出來搭伤,本次實踐中暫不做討論只怎。
盡管MVVM
如此優(yōu)秀,但是任然需要注意很多細節(jié)怜俐,否則就會重蹈MVC
胖Controller
的覆轍身堡,我總結(jié)了下,大概有以下幾點需要注意的:
???(1). Controller
中盡量不要做業(yè)務(wù)相關(guān)的事情拍鲤,它的主要職責是管理subview
和進行View
和ViewModel
的綁定贴谎;
???(2). 業(yè)務(wù)邏輯盡量讓ViewModel
來處理。同時應該避免ViewModel
過于龐大而導致和MVC
相同的問題季稳,所以ViewModel
之間是可以存在依賴的擅这;
???(3). subview
是否能夠直接引用ViewModel
?既然UIViewController
和UIView
都是View
層的組件景鼠,所以是可以引用的仲翎。但是反過來卻不行,因為一旦ViewModel
包含了View
,就會導致View
產(chǎn)生耦合谭确,代碼復用性和可測試性都會降低帘营;
???(4). ViewModel
可以引用Model
。但是反過來卻不行逐哈,原因同上芬迄;
函數(shù)響應式編程
通常我們在開發(fā)中會建立很多網(wǎng)狀關(guān)系,代碼如下:
int a = 1;
int b = a + 1;
print(b); // b = 2
a = 5;
print(b); // b = 2
這樣的代碼沒有任何問題昂秃,但是我們想要的卻是變量a
和變量b
形成對應關(guān)系禀梳,當a
改變時應該觸發(fā)b
的更新操作。在沒有響應式的前提下肠骆,需要花精力去維護一套這樣的“關(guān)系”算途,無疑會增加開發(fā)成本。而響應式編程就是想通過某些操作來幫我們構(gòu)建這種關(guān)系:
int a = 1;
int b <- a + 1; // <- 是響應式中定義的一種操作
print(b); // b = 2
a = 5;
print(b); // b = a + 1 = 6
所以響應式編程的核心就是通過定義好的操作來建立這種關(guān)系蚀腿,而不是通過賦值等嘴瓤。響應式編程就是一種通過異步和數(shù)據(jù)流來建立事務(wù)關(guān)系的編程模型。它是一種基于事件的莉钙,專注于數(shù)據(jù)流和變化傳遞的編程范式廓脆。
而函數(shù)響應式編程就是通過函數(shù)來創(chuàng)建這種“關(guān)系”,并在適當?shù)牡胤巾憫@種“關(guān)系”磁玉,從而得到正確的結(jié)果停忿。
在眾多函數(shù)響應式的框架中ReactiveX
應該算是代表了。它是一套完整的跨平臺的解決方案蚊伞,而RxSwift
則是它對iOS/macOS
平臺支持的三方庫席赂。在RxSwift
中最核心的包括:
???(1). Observable - 產(chǎn)生事件;
???(2). Observer - 響應事件时迫;
???(3). Operator - 創(chuàng)建變化組合事件颅停;
???(4). Disposable - 管理綁定(訂閱)的生命周期;
???(5). Schedulers - 線程隊列調(diào)配掠拳;
這里假設(shè)你對RxSwift
有一定的了解便监,理應知道這5大核心的左右和如何使用,如若不清楚的碳想,可以通過連接自行查看中文教程。
我們有說到Rx
改變了常見的響應方式毁靶,包括Target-Action
胧奔、Delegate
、Notification
预吆、Block
等等龙填。從代碼層面是如何體現(xiàn)的呢:
// MARK: Target-Action 使用 RxSwift 實現(xiàn)的對比
button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside)
func buttonTapped() {
print("button Tapped")
}
// Rx
button.rx.tap
.subscribe(onNext: {
print("button Tapped")
})
.disposed(by: disposeBag)
// MARK: Delegate 使用 RxSwift 實現(xiàn)的對比
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
scrollView.delegate = self
}
}
extension ViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) {
print("contentOffset: \(scrollView.contentOffset)")
}
}
// Rx
class ViewController: UIViewController {
...
override func viewDidLoad() {
super.viewDidLoad()
scrollView.rx.contentOffset
.subscribe(onNext: { contentOffset in
print("contentOffset: \(contentOffset)")
})
.disposed(by: disposeBag)
}
}
// MARK: 閉包或 Block 使用 RxSwift 實現(xiàn)的對比
URLSession.shared.dataTask(with: URLRequest(url: url)) {
(data, response, error) in
guard error == nil else {
print("Data Task Error: \(error!)")
return
}
guard let data = data else {
print("Data Task Error: unknown")
return
}
print("Data Task Success with count: \(data.count)")
}.resume()
// Rx
URLSession.shared.rx.data(request: URLRequest(url: url))
.subscribe(onNext: { data in
print("Data Task Success with count: \(data.count)")
}, onError: { error in
print("Data Task Error: \(error)")
})
.disposed(by: disposeBag)
// MARK: Notification 使用 RxSwift 實現(xiàn)的對比
var ntfObserver: NSObjectProtocol!
override func viewDidLoad() {
super.viewDidLoad()
ntfObserver = NotificationCenter.default.addObserver(
forName: .UIApplicationWillEnterForeground,
object: nil, queue: nil) { (notification) in
print("Application Will Enter Foreground")
}
}
deinit {
NotificationCenter.default.removeObserver(ntfObserver)
}
// Rx
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.rx
.notification(.UIApplicationWillEnterForeground)
.subscribe(onNext: { (notification) in
print("Application Will Enter Foreground")
})
.disposed(by: disposeBag)
}
// MARK: 多任務(wù)依賴使用 RxSwift 實現(xiàn)的對比
API.token(username: "beeth0ven", password: "987654321",
success: { token in
API.userInfo(token: token,
success: { userInfo in
print("獲取用戶信息成功: \(userInfo)")
},
failure: { error in
print("獲取用戶信息失敗: \(error)")
})
},
failure: { error in
print("獲取用戶信息失敗: \(error)")
})
// Rx
API.token(username: "beeth0ven", password: "987654321")
.flatMapLatest(API.userInfo)
.subscribe(onNext: { userInfo in
print("獲取用戶信息成功: \(userInfo)")
}, onError: { error in
print("獲取用戶信息失敗: \(error)")
})
.disposed(by: disposeBag)
// MARK: 等待多個并發(fā)任務(wù)完成后處理結(jié)果使用 RxSwift 實現(xiàn)
Observable.zip(
API.teacher(teacherId: teacherId),
API.teacherComments(teacherId: teacherId)
).subscribe(onNext: { (teacher, comments) in
print("獲取老師信息成功: \(teacher)")
print("獲取老師評論成功: \(comments.count) 條")
}, onError: { error in
print("獲取老師信息或評論失敗: \(error)")
})
.disposed(by: disposeBag)
代碼從來都不會說謊。RxSwift
從維護性、易讀性甚至代碼量上都遠優(yōu)于未使用時岩遗,對比十分明顯扇商。特別是對異步操作時簡化了代碼邏輯,統(tǒng)一了代碼風格宿礁,而我們只需要專注的做業(yè)務(wù)相關(guān)的研發(fā)就可以了案铺。同時,這也是為什么我們要使用它的原因梆靖。
那么Rx
是不是就是完美的呢控汉?我認為至少存在以下問題:
???(1). 學習Rx
的難度相對于老方法要難得多,學習成本大返吻;
???(2). 不便于調(diào)試姑子。原來出現(xiàn)問題會在固定的地方發(fā)現(xiàn),而現(xiàn)在經(jīng)過 Rx
傳遞后可能被轉(zhuǎn)移测僵;
想要了解并學習更多關(guān)于RxSwift
的知識可以在這里獲得 - RxSwift中文教程街佑。
登錄的實踐
和普通登錄頁面一樣,賬號和密碼通過對應的正則判斷后控制了登錄按鈕的交互狀態(tài)捍靠。點擊登錄后登錄按鈕關(guān)閉交互狀態(tài)并禁止頁面上的其他控件產(chǎn)生交互沐旨,如下圖所示:
在MVVM
設(shè)計模式中,ViewModel
的實現(xiàn)至關(guān)重要剂公。良好的ViewModel
的設(shè)計將直接影響程序的維護性和可測試性等希俩。在這篇文章中對ViewModel
的實現(xiàn)有十分重要的指導作用,我也是根據(jù)它的原理來設(shè)計和實現(xiàn)的 - 怎么搞定ViewModel纲辽。
不難看出颜武,在本例中Inputs
是:
???(1). 手機號輸入事件;
???(2). 密碼輸入事件拖吼;
???(3). 登錄按鈕點擊事件鳞上;
Outputs
是:
???(1). 登錄狀態(tài),控制登錄按鈕的UI
呈現(xiàn)吊档。在正常狀態(tài)下顏色亮一些篙议,不可點擊時顏色淺,登錄執(zhí)行中存在菊花怠硼;
???(2). 賬號和密碼檢測鬼贱,項目中沒有用到,將來可能會在UI
上體現(xiàn)出來香璃;
???(3). 登錄中这难,用戶不能和登錄頁面進行交互;
???(4). 登錄結(jié)果葡秒;
從而姻乓,得出Inputs
和Outputs
應該長這樣:
inputs: (
account: Driver<String>,
password: Driver<String>,
loginTaps: Signal<()>
)
struct Outputs {
// 賬號和密碼的驗證
let validatedAccount: Driver<SignValidationResult>
let validatedPassword: Driver<SignValidationResult>
let signupState: Driver<ActivityButton.State> // 登錄狀態(tài)
let signedIn: Driver<Bool> // 登錄結(jié)果
let signingIn: Driver<Bool> // 登錄中
}
這時候嵌溢,我們可以通過服務(wù)的方式提供具體的驗證實現(xiàn)和網(wǎng)絡(luò)接口:
enum SignValidationResult {
case ok(message: String)
case empty
case failed(message: String)
}
extension SignValidationResult {
var isValid: Bool {
switch self {
case .ok:
return true
default:
return false
}
}
}
// 提供登錄賬號和登錄密碼正則判斷的驗證服務(wù)
protocol SignValidationService {
func validateAccount(_ account: String) -> SignValidationResult
func validatePassword(_ password: String) -> SignValidationResult
}
// 提供登錄接口的服務(wù)
protocol MxsSignAPI {
func signup(_ account: String, password: String) -> Observable<Bool>
}
在驗證服務(wù)中,由于項目本身使用的是本地正則來判斷是否合法蹋岩,是能夠立即得到結(jié)果而并非異步等待赖草,所以直接返回的驗證結(jié)果的枚舉。當這里的驗證是由服務(wù)端執(zhí)行的剪个,那么在和服務(wù)端交互時秧骑,驗證返回的應該是一個事件Observable <SignValidationResult>
,這樣才能統(tǒng)一異步序列禁偎,否則就會出現(xiàn)因為異步開發(fā)導致的一些不太好處理的問題腿堤。當然,之所以使用協(xié)議來定義這些服務(wù)是想要增加擴展性和穩(wěn)定性如暖,當默認實現(xiàn)不滿足需求時笆檀,可以通過新的實現(xiàn)來擴展功能,而無需對已存在的接口進行修改盒至,只要滿足條件酗洒,那么修改的就只有協(xié)議的實現(xiàn)部分,其他任然保持原樣(PS:協(xié)議的默認實現(xiàn)我就不給出來了枷遂,留給你們想象的空間>_<) 樱衷。這樣我們可以得出一個小技巧:異步操作時使用Observable
,否則服務(wù)方直接返回結(jié)果酒唉。
登錄是一個異步的網(wǎng)絡(luò)過程矩桂,當?shù)卿浾埱笳谟|發(fā)工程中時,輸入框和按鈕都是無法交互的痪伦。所以需要有一個東西能夠監(jiān)聽到請求服務(wù)的狀態(tài)變化侄榴,ActivityIndicator
正好能夠做到這樣的事情。ActivityIndicator
服從協(xié)議SharedSequenceConvertibleType
网沾,直接調(diào)用asObservable()
就可以得到_loading
的狀態(tài)癞蚕。
_loading
可以看成一個發(fā)射器,當_relay
行為的值為0是發(fā)送一個false
的值辉哥,當_relay
行為值大于0時發(fā)送一個true
的值桦山,表明當前還有Observable
正在執(zhí)行。再通過increment
和decrement
來處理Observable
的數(shù)量:
private let _relay = BehaviorRelay(value: 0)
private let _loading: SharedSequence<SharingStrategy, Bool>
_loading = _relay.asDriver()
.map { $0 > 0 }
.distinctUntilChanged()
private func increment() {
_lock.lock()
_relay.accept(_relay.value + 1)
_lock.unlock()
}
private func decrement() {
_lock.lock()
_relay.accept(_relay.value - 1)
_lock.unlock()
}
為了能夠?qū)?code>ActivityIndicator和需要監(jiān)聽的Observable
綁定醋旦,專門為Observable
擴展了一個trackActivity()
的方法恒水,傳入一個ActivityIndicator
來跟蹤Observable
的狀態(tài):
public func trackActivity(_ activityIndicator: ActivityIndicator) -> Observable<Element> {
return activityIndicator.trackActivityOfObservable(self)
}
class ActivityIndicator {
fileprivate func trackActivityOfObservable<Source: ObservableConvertibleType>(_ source: Source) -> Observable<Source.Element> {
return Observable.using({ () -> ActivityToken<Source.Element> in
self.increment()
return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
}) { t in
return t.asObservable()
}
}
}
通過調(diào)用Observable
的using
方法將increment
和decrement
與Observable
的生命周期綁定。當using
調(diào)用resourceFactory
時執(zhí)行increment
饲齐,當dispose
時執(zhí)行decrement
寇窑,這樣就可以檢測有多少個Observable
處于正在處理的狀態(tài)了。
至此箩张,ViewModel
實現(xiàn)代碼如下:
class SignupViewModel {
struct Outputs {
let validatedAccount: Driver<SignValidationResult>
let validatedPassword: Driver<SignValidationResult>
let signupState: Driver<ActivityButton.State>
let signedIn: Driver<Bool>
let signingIn: Driver<Bool>
}
let outputs: Outputs
init(
inputs: (
account: Driver<String>,
password: Driver<String>,
loginTaps: Signal<()>
),
dependency: (
API: MxsSignAPI,
validationService: SignValidationService
) = (MxsSignDefaultAPI(), SignValidationDefaultService())
) {
let API = dependency.API
let validationService = dependency.validationService
let accountAndPassword = Driver.combineLatest(inputs.account, inputs.password) { ($0, $1) }
let activity = ActivityIndicator()
let signingIn = activity.asDriver()
let validatedAccount = inputs.account
.map { account in
return validationService.validateAccount(account)
}
let validatedPassword = inputs.password
.map { password in
return validationService.validatePassword(password)
}
let signedIn = inputs.loginTaps.withLatestFrom(accountAndPassword)
.flatMapLatest {
return API.signup($0.0, password: $0.1)
.trackActivity(activity)
.asDriver(onErrorJustReturn: false)
}
let signupState = Driver.combineLatest(
validatedAccount, validatedPassword, signingIn
) { username, password, signingIn -> ActivityButton.State in
if username.isValid && password.isValid {
if signingIn {
return ActivityButton.State.activiting
} else {
return ActivityButton.State.default
}
} else {
return ActivityButton.State.disable
}
}
.distinctUntilChanged()
outputs = Outputs(validatedAccount: validatedAccount,
validatedPassword: validatedPassword,
signupState: signupState,
signedIn: signedIn,
signingIn: signingIn)
}
}
將賬號(account
)和密碼(password
)的輸入利用combineLatest
組合起來得到賬號密碼的組合序列accountAndPassword
甩骏,只要賬號和密碼中的任意一個改變都會觸發(fā)一次事件。將賬號和密碼的輸入先慷,利用map
和提供的服務(wù)饮笛,轉(zhuǎn)化為校驗結(jié)果的序列validatedAccount
和validatedPassword
。當發(fā)送網(wǎng)絡(luò)請求時论熙,利用ActivityIndicator
監(jiān)聽請求狀態(tài)福青,再和校驗結(jié)果的信號組合,從而得到登錄按鈕當前所屬狀態(tài)脓诡。最后將得到的信號包裝為Outputs
提供給Controller
用于綁定或事件无午,完成了ViewModel
的功能。
番外
在這次重構(gòu)中祝谚,一邊學習RxSwift
一邊整理宪迟,真正體會到了MVVM
的精髓。由于登錄業(yè)務(wù)還是比較簡單的交惯,還不能以偏概全的認為“就是這樣”次泽,在重構(gòu)過程中,后面會遇到真正的難點席爽,對于復雜頁面和復雜業(yè)務(wù)的實現(xiàn)意荤。到時候再以續(xù)集的博客形式記錄。
最后附上本次RxSwift
官方的例子作為參考:GitHubSignup - 簡易仔細閱讀