上個(gè)月某天和往常一樣清蚀、突然就被拉入了一個(gè)群, 新項(xiàng)目, 用 SwiftUI !
終于跟上潮流了啊, 但此時(shí)對(duì) SwiftUI 的印象幾乎剩下它的名字...
“立刻開發(fā)”、 “馬上就要”
那時(shí)候, 我的內(nèi)心只剩下:
閱讀難度: [簡單]
要求: 了解 SwiftUI车摄、常用設(shè)計(jì)模式
本文是一次 SwiftUI 項(xiàng)目中對(duì)于 MVVM 架構(gòu)適配實(shí)際業(yè)務(wù)場景的記錄, 不構(gòu)成任何開發(fā)建議
閑話少說、直接開干
又雙叒叕快速刷了一遍蘋果官方的 SwiftUI 和部分 WWDC 教程, 這次印象最深的是兩個(gè)屬性包裝器
@Observable
和@Environment
, 這也開啟了與MVI
的偶遇之旅.
@Observable: iOS17 支持 , 為自定義類型添加觀察, 使其支持 Observable 協(xié)議, 省去了老版本中的樣板代碼.
@Environment: 環(huán)境值, 自定義環(huán)境值需與 .environment() 配套使用. 使得我們?cè)谝晥D樹中可以共享/訪問特定的環(huán)境值.
原本 APP 上業(yè)務(wù)多而復(fù)雜、更新迭代非乘辈ィ快, APP 中從邏輯上獨(dú)立了事件流, 這在一定程上減小多人合作中可能會(huì)出現(xiàn)的并行開發(fā)邏輯混亂的問題. 但依舊存在出現(xiàn)狀態(tài)混亂的可能性. 在只求迭代速度的前提下, APP 出于“穩(wěn)定性”变屁、“成本”的考慮, 幾乎不能從架構(gòu)上來大刀闊斧的重構(gòu).
這次是全新的項(xiàng)目, 這倆屬性包裝器讓我萌生了一個(gè)在 APP 側(cè)想做卻苦于沒有機(jī)會(huì)的想法. 為了追求速度, 最初始的架構(gòu)設(shè)計(jì)上依舊以 MVVM 為基礎(chǔ). 有了 @Observable
的助力, 使得 ViewModel 與 View 的雙向綁定
更加簡化, 再通過 @State
、@Bindable
與 @Observable
的關(guān)聯(lián)意狠、 SwiftUI 局部刷新也更加高效.
每個(gè)事件的處理都可能非常復(fù)雜, 涉及到的事件粟关、交互成百上千. 一開始習(xí)慣性的通過拆分業(yè)務(wù)來避免 viewModel 過于臃腫, 可隨著業(yè)務(wù)越來越多越來越復(fù)雜, 各類“子ViewModel”
越來越多, 關(guān)聯(lián)邏輯越來越復(fù)雜冗余, 而這樣的模塊基本不會(huì)是1個(gè)人開發(fā), 不可避免的, 又將滑向了屎山
(Oh ! holy...shit).
有了APP的前車之鑒, 這次一開始就隔離出了事件流, 為后續(xù)的改進(jìn)省去了不少工作量. 我們用簡單的訂單列表頁面為例子:
// VM
@Observable
class OrderDetailVM {
var statusSectionModel:OrderDetailStatusSectionModel?
var infoCardSectionModel:OrderDetailInfoCardSectionModel?
var feeDetailSectionModel:OrderDetailFeeDetailSectionModel?
var schedulingSectionModel:OrderDetailSchedulingSectionModel?
// ...
}
extension OrderDetailVM {
enum OrderDetailEvent {
case statusButton(Int?)
case refreshAllData
//...
}
func runEvent(event: OrderListEvent) {
switch event {
case .statusButton(let actionID):
solveStatusButton(buttonActionID: actionID)
break
case .refreshAllData:
fetchAll()
break
//.....
}
}
}
// View
struct OrderDetailView: View {
@State var viewModel = OrderDetailVM()
var body: some View {
VStack{
OrderDetailStatusSectionView(viewModel: viewModel.statusSectionModel)
//....
}
.environment(viewModel)
}
}
struct OrderDetailStatusSectionView: View {
@Binding var sectionModel: OrderDetailStatusSectionModel?
@Environment(OrderDetailVM.self) private var viewModel
var body: some View {
//...
HStack {
if let buttons = sectionModel.actions {
ForEach(buttons, id: \.actionId) { button in
Button {
viewModel?.runEvent.statusButton(button.actionId)
} label: {
Text("測試按鈕")
}
}
}
}
// ...
}
}
上面的代碼我們可以發(fā)現(xiàn):
- 結(jié)構(gòu)上, View 層能夠直接與 model 層通信, 不太嚴(yán)謹(jǐn)
- 事件流還是歸屬于 ViewModel, 并未將其從架構(gòu)上獨(dú)立出來
根源都在于“事件流僅僅在邏輯上進(jìn)行了區(qū)分”
,
視圖層只需要通過事件流告知 ViewModel 變化, 針對(duì)這一點(diǎn)可以將事件流協(xié)議化, 使得 View 層強(qiáng)制遵循事件流的協(xié)議進(jìn)行通信. 并將所有與 ViewModel 的通信都經(jīng)過事件流處理, 從架構(gòu)上限制業(yè)務(wù).
優(yōu)化優(yōu)化:
// 事件流基礎(chǔ)協(xié)議
protocol EventBusProtocol: AnyObject {
associatedtype ViewModelType
associatedtype EventBusType
var eventBusVM: ViewModelType { get }
func runEvent(_ event: EventBusType)
}
// 訂單詳情事件協(xié)議及實(shí)現(xiàn)
protocol OrderDetailVMEventProtocol: EventBusProtocol where ViewModelType == OrderDetailVM, EventBusType == OrderDetailEvent {
}
// 關(guān)聯(lián)實(shí)現(xiàn)事件流對(duì)應(yīng)的VM
extension OrderDetailVM: OrderDetailVMEventProtocol {
var eventBusVM: OrderDetailVM {
self
}
}
// 在協(xié)議擴(kuò)展中實(shí)現(xiàn)具體的處理
extension OrderDetailVMEventProtocol {
func runEvent(_ event: EventBusType) {
switch event {
case .statusButton(let actionID):
solveStatusButton(buttonActionID: actionID)
break
case .refreshAllData:
eventBusVM.fetchAll()
break
//...
}
}
}
這樣改動(dòng)后:
- 對(duì)事件流進(jìn)行了協(xié)議式分離, View 層僅通過環(huán)境值來調(diào)用事件協(xié)議聲明的方法. 杜絕了View直接與Model的通信.
- 完全由數(shù)據(jù)流驅(qū)動(dòng), 使應(yīng)用
狀態(tài)
管理更加明確
和可預(yù)測
- 面向協(xié)議的方式也使得事件流方式更加
通用化
,易理解
, 也擁有較好的擴(kuò)展性
, 無論是嚴(yán)格按照事件流來處理交互響應(yīng)、或是只有某一部分采用事件流的方式都能很好的適配.
相應(yīng)的, View層上更新下對(duì)應(yīng)環(huán)境值的獲取與調(diào)用即可, 同樣以上面的訂單狀態(tài)視圖為例:
struct OrderDetailView: View {
@State var viewModel = OrderDetailVM()
var body: some View {
VStack{
OrderDetailStatusSectionView(viewModel: viewModel.statusSectionModel)
//....
}
.environment(\.orderDetailEventBus, viewModel)
}
}
struct OrderDetailStatusSectionView: View {
@Binding var sectionModel: OrderDetailStatusSectionModel?
@Environment(\.orderDetailEventBus) private var viewModel
var body: some View {
//...
HStack {
if let buttons = sectionModel.actions {
ForEach(buttons, id: \.actionId) { button in
Button {
viewModel?.runEvent.statusButton(button.actionId)
} label: {
Text("測試按鈕")
}
}
}
}
// ...
}
}
這怎么有點(diǎn)像 MVI ? 我們的“事件流”(EventBus)實(shí)際上與MVI中的“意圖”(Intent)一樣, 都有著“單向數(shù)據(jù)流”
的特點(diǎn).
上述的改進(jìn)細(xì)節(jié)上還需優(yōu)化, 并且保留著 ViewModel . 雖然有了“MVI”中“單一狀態(tài)流”的概念, 也只是協(xié)議化了事件流, 依舊在ViewModel 層上.
[ MVVM ]: ViewModel負(fù)責(zé)處理與UI無關(guān)的業(yè)務(wù)邏輯, 并提供數(shù)據(jù)供視圖View顯示. 使用數(shù)據(jù)綁定來保持ViewModel 與View之間的同步
MVVM.png
[ MVI ]: 側(cè)重于通過Intent來驅(qū)動(dòng)應(yīng)用程序的狀態(tài)變化. 通常包含一個(gè)單一的狀態(tài)來作為數(shù)據(jù)流的核心, 通過處理Intent來更新狀態(tài), 并通過訂閱來更新View
MVI.png
反過來想, 把我們現(xiàn)在的ViewModel層看作是Model層环戈、而Intent層, 是ViewModel被抽象成了協(xié)議的事件流部分.從模塊通信上看, Intent包含著 ViewModel 層, 那就假裝它是Intent層吧.
或者, 把 MVI 當(dāng)作是 ViewModel 帶了個(gè)Intent 帽子的 MVVM. 應(yīng)該也能湊合說的過去吧.
結(jié)語
一個(gè) View 可以搞定靜態(tài)頁面, 經(jīng)久耐用的MVVM , 分工協(xié)作更加細(xì)化獨(dú)立的VIPER......
有的架構(gòu)模糊了部分模塊之間的界限, 有的架構(gòu)需要大量成本來維持其自身的通信規(guī)范. 這次與 MVI 的偶遇也再次加深了自己的看法:
架構(gòu)是重構(gòu)過程思想的重要體現(xiàn), 而重構(gòu)是需要一直進(jìn)行的.
“沒有最好的架構(gòu)闷板、只有目前最合適的”