對沸點頁面仿寫的補充-RxSwift

語言: CN / TW / HK

0.JPEG

# 前言

誠如我在 上篇 中所說的,最近被大佬教育過沒有實現 雙向資料繫結 就不能成為完整 MVVM 架構。因此這篇文章中所說的 MVVM 僅代表表個人所理解的 iOS中的 MVVM 架構,如有不對之處請海涵(~~您老總不能來打我吧🤪~~)。

# 個人理解的 MVVM

在之前(2020年)寫另一篇文章 的時候,剛學習使用 RxSwift 參考了一寫文章發現大家對 ViewModel 層的使用有不同的方式,常見的為:

1) ViewModel 的建立需要外部的引數,ViewModel 實際作用就是轉換 input 輸出一個 output

2) ViewModel 的初始化不依賴於外部引數;ViewModel 對外提供函式呼叫,內部將此呼叫轉換為一個 Observable<T> 的輸出。

這裡不會對兩種方式的優劣進行評價,本人在實際專案中使用的是第二種方式,並使用協議區分了 InputOutput,示例如下:

```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 使用的糾正

在原文中,我們簡單的使用了 MoyaRxSwift,但實際專案中的網路請求和邏輯處理會比現在更為複雜,例如:要在讀取首頁資料的同時獲取 推薦圈子 資料,僅在載入特定頁(如第二頁等)的資料時讀取 推薦沸點 資料,增加 熱門話題 等。以下僅以增加 推薦圈子 功能給與舉例說明:

  1. 新增 推薦圈子 的介面呼叫和相關介面的展示。

  2. 如果 推薦圈子 讀取失敗則不展示相應的檢視。

  3. 推薦圈子 不影響現有的功能。

您可以先思考下實現次需求要改動的地方。

我們先把原文中 RxSwift 使用不合理的地方進行修改。

# compactMapmap

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 功能一致。

# XxxxSubjecterror

在使用 RxSwift 中一定要十分注意 不要 輕易使用 Observer.error(xxx),特別是在 flatMapOperator 中。例如原文 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> in let result = DynamicNetworkService.list(param: param.toJsonDict()) .request() .map(XTListResultModel.self) .map { model -> Result in .success(model) }.catch { .just(.failure($0)) }

return result.asObservable()

} ```

這樣 loadDataSubjectRxMoya 出現網路請求、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() 中增加對 topicListSubjectflatMap {} 操作

```Swift let topicListData = topicListSubject.flatMap { _ -> Observable> in let result = DynamicNetworkService.topicListRecommend .memoryCacheIn() .request() .map(TopicListModel.self) .flatMap { model -> Single> in .just(.success(model)) }.catch { .just(.failure($0)) }

return result.asObservable()

} `` 然後對topicListDatadynamycData,進行zip` 操作:

```Swift let dynamycData = loadDataAction.filter { ... }

let topicListData = topicListSubject.flatMap { ... }

let newDataSubject = Observable.zip(dynamycData, topicListData).map { (dynamicWrapped, topicListWrapped) -> Result in var displayModel = DynamicDisplayModel()

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,但這裡面確實麼得需要修改的地方,最多就是後續新增 DynamicTopicWrapperCellNodedelegate 方法。

# 補充

  • 對於網路層的封裝可參閱對沸點頁面仿寫的補充-網路層,當然現在的原始碼中已經包含了此部內容。

  • demo 最低版本要求 iOS 13 這並不是 Rx 等三方庫導致,你完全可以調整到 iOS 10,並重新 pod install。使用 iOS 13 僅僅是想在後續替換掉 Rx