對沸點頁面仿寫的補充-RxSwift
# 前言
誠如我在 上篇 中所說的,最近被大佬教育過沒有實現 雙向資料繫結 就不能成為完整 MVVM
架構。因此這篇文章中所說的 MVVM
僅代表表個人所理解的 iOS
中的 MVVM
架構,如有不對之處請海涵(~~您老總不能來打我吧🤪~~)。
# 個人理解的 MVVM
在之前(2020年)寫另一篇文章 的時候,剛學習使用 RxSwift
參考了一寫文章發現大家對 ViewModel
層的使用有不同的方式,常見的為:
1) ViewModel
的建立需要外部的引數,ViewModel
實際作用就是轉換 input
輸出一個 output
。
2) ViewModel
的初始化不依賴於外部引數;ViewModel
對外提供函式呼叫,內部將此呼叫轉換為一個 Observable<T>
的輸出。
這裡不會對兩種方式的優劣進行評價,本人在實際專案中使用的是第二種方式,並使用協議區分了 Input
和 Output
,示例如下:
```Swift protocol DynamicListViewModelInputs {
func viewDidLoad()
func refreshDate()
func moreData(with cursor: String, needHot: Bool)
}
protocol DynamicListViewModelOutputs {
var refreshData: Observable<DynamicDisplayModel> { get }
var moreData: Observable<DynamicDisplayModel> { get }
var endRefresh: Observable<Void> { get }
var hasMoreData: Observable<Bool> { get }
var showError: Observable<String> { get }
}
protocol DynamicListViewModelType { var input: DynamicListViewModelInputs { get } var output: DynamicListViewModelOutputs { get } }
final class DynamicListViewModel: DynamicListViewModelType, DynamicListViewModelInputs, DynamicListViewModelOutputs { ... ... } ```
外部使用:
```Swift // 建立 VM private let viewModel: DynamicListViewModelType = DynamicListViewModel() private let dataSource = DynamicListDataSource() ....
override func viewDidLoad() { super.viewDidLoad() ...
// 呼叫 input
viewModel.input.viewDidLoad()
} ...
func bindViewModel() {
// 訂閱 output
viewModel.output.refreshData.subscribe(onNext: { [weak self] wrappedModel in
self?.dataSource.newData(from: wrappedModel)
self?.tableNode.reloadData()
}).disposed(by: disposeBag)
...
} ```
這樣 ViewModel
也做到了介面隔離,同樣的 ViewModel
中並沒有儲存資料和狀態變數,對於 TableNode
需要的資料抽離 DataSource
進行儲存儲存,進一步的分離了 ViewModel
的職責。ViewModel
只進行資料的加工,DataSource
負責資料的儲存和提供中間狀態。
如上所屬 MVVM
雖然會增加型別,但其提高了程式碼可讀性和可維護性。
# 對 RxSwift 使用的糾正
在原文中,我們簡單的使用了 Moya
和 RxSwift
,但實際專案中的網路請求和邏輯處理會比現在更為複雜,例如:要在讀取首頁資料的同時獲取 推薦圈子 資料,僅在載入特定頁(如第二頁等)的資料時讀取 推薦沸點 資料,增加 熱門話題 等。以下僅以增加 推薦圈子 功能給與舉例說明:
-
新增 推薦圈子 的介面呼叫和相關介面的展示。
-
如果 推薦圈子 讀取失敗則不展示相應的檢視。
-
推薦圈子 不影響現有的功能。
您可以先思考下實現次需求要改動的地方。
我們先把原文中 RxSwift
使用不合理的地方進行修改。
# compactMap
和 map
原 ListViewModel
中的
Swift
private let loadDataSubject: BehaviorSubject<String?> = BehaviorSubject(value: nil)
...
let loadDataAction = self.loadDataSubject.filter { $0 != nil }.map { string -> String in
guard let cursor = string else { fatalError("") }
return cursor
}
修改為:
Swift
let loadDataAction = loadDataSubject.compactMap { $0 }
compactMap
本身就是用來去除 nil
這和 Swift.Collection
功能一致。
# XxxxSubject
和 error
在使用 RxSwift
中一定要十分注意 不要 輕易使用 Observer
的 .error(xxx)
,特別是在 flatMap
等 Operator
中。例如原文 ListViewModel
中:
Swift
loadDataAction.filter { $0 == "0" }.map { cursor -> DynamicListParam in
return DynamicListParam(cursor: cursor)
}.flatMap { param -> Single<XTListResultModel> in
// 註釋1
return DynamicNetworkService.list(param: param.toJsonDict())
.request()
.map(XTListResultModel.self)
}
在 註釋1
處如果網路出現波動導致 Rx
+ Moya
(下文統稱 RxMoya
) 中丟擲 error
事件,而我們直接將自己的 loadDataSubject
轉換了成了 RxMoya
生成的 Single<T: Codable>
。因此一旦有 error
事件,就是導致 loadDataSubject
終止事件流的傳遞,不在傳送新的元素。所以這裡需要對 RxMoya
進行一次 catch error
處理,而後續的組合中我們又需要這個 error
資訊,因此就要對結果使用 Result<T,Error>
進行一次包裹,程式碼如下(存在省略寫法):
```Swift
let dynamycData = loadDataAction.filter { $0 != "0" }.map { cursor -> DynamicListParam in
DynamicListParam(cursor: cursor)
}.flatMap { param -> Observable
return result.asObservable()
} ```
這樣 loadDataSubject
在 RxMoya
出現網路請求、Model Decoder
等錯誤時也不會被終止。
# Model 的變動
現在 UI
介面需要展示不同型別的 cell
,因此我們需要對 DynamicListModel
進行一次包裹:
```Swift enum DynamicDisplayType { case dynamic(DynamicListModel) case topicList([TopicModel]) case hotList([DynamicListModel]) }
/// 對應 XTListResultModel struct DynamicDisplayModel {
var cursor: String? = nil
var errMsg: String? = nil
var errNo: Int? = nil
var displayModels: [DynamicDisplayType] = []
var hasMore: Bool = false
var dynamicsCount: Int = 0
....
} ```
# VM中增加資料請求和處理
有了以上調整,我們實現 推薦圈子 功能就可以正式動工了,將請求首頁沸點的 Observe
單獨定義,如上述處理 error
程式碼所示,在 ListViewModel
中增加對應的 PublishSubject
```Swift
// 增加圈子資料請求
private let topicListSubject = PublishSubject
func loadFirstPageData() { topicListSubject.onNext(()) loadDataSubject.onNext("0") } ```
將 loadDataSubject.onNext("0")
替換為 loadFirstPageData()
。
在 func initializedNewDateSubject()
中增加對 topicListSubject
的 flatMap {}
操作
```Swift
let topicListData = topicListSubject.flatMap { _ -> Observable
return result.asObservable()
}
``
然後對
topicListData和
dynamycData,進行
zip` 操作:
```Swift let dynamycData = loadDataAction.filter { ... }
let topicListData = topicListSubject.flatMap { ... }
let newDataSubject = Observable.zip(dynamycData, topicListData).map { (dynamicWrapped, topicListWrapped) -> Result
switch dynamicWrapped {
case .success(let wrapped):
displayModel = DynamicDisplayModel.init(from: wrapped)
case .failure(let error):
return .failure(error)
}
switch topicListWrapped {
case .success(let wrapped):
if let list = wrapped.data, !list.isEmpty {
displayModel.displayModels.insert(.topicList(list), at: 0)
}
case .failure(let error):
// FIXED: - 請求或者解析資料失敗, 不作任何處理, 介面不展示
print(error)
}
return .success(displayModel)
} ```
# DataSource 中的調整
至此網路和資料處理部分結束,下面改造 DataSource
,這裡更簡單隻需要將 XTListResultModel
替換為 DynamicDisplayModel
,將 DynamicListModel
替換為 DynamicDisplayType
,然後在 ASTableDatasource
中修改以下程式碼:
```Swift func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { let model = commendList[indexPath.row]
switch model {
case .dynamic(let dynModel):
let cellNode = DynamicListCellNode()
cellNode.configure(with: dynModel)
return cellNode
case .topicList(let topic):
let cellNoed = DynamicTopicWrapperCellNode()
cellNoed.configure(with: topic)
return cellNoed
case .hotList(let list):
// TODO: - 這裡需要替換為 DynamicHotListWrapperCellNode
let cellNode = DynamicListCellNode()
cellNode.configure(with: list[0])
return cellNode
}
} ```
所有操作結束,功能實現。
嘿!!!我並沒有忘記 ViewController
,但這裡面確實麼得需要修改的地方,最多就是後續新增 DynamicTopicWrapperCellNode
的 delegate
方法。
# 補充
-
對於網路層的封裝可參閱對沸點頁面仿寫的補充-網路層,當然現在的原始碼中已經包含了此部內容。
-
demo
最低版本要求iOS 13
這並不是Rx
等三方庫導致,你完全可以調整到iOS 10
,並重新pod install
。使用iOS 13
僅僅是想在後續替換掉Rx
。