# 前语
诚如我在 上篇 中所说的,最近被大佬教育过没有完成 双向数据绑定 就不能成为完好 MVVM
架构。因而这篇文章中所说的 MVVM
仅代表表个人所了解的 iOS
中的 MVVM
架构,如有不对之处请海涵(您老总不能来打我吧)。
# 个人了解的 MVVM
在之前(2020年)写另一篇文章 的时分,刚学习运用 RxSwift
参看了一写文章发现大家对 ViewModel
层的运用有不同的方式,常见的为:
-
ViewModel
的创建需求外部的参数,ViewModel
实践效果就是转化input
输出一个output
。 -
ViewModel
的初始化不依赖于外部参数;ViewModel
对外供给函数调用,内部将此调用转化为一个Observable<T>
的输出。
这儿不会对两种方式的好坏进行点评,本人在实践项目中运用的是第二种方式,并运用协议区分了 Input
和 Output
,示例如下:
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 {
...
...
}
外部运用:
// 创建 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
中的
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
}
修正为:
let loadDataAction = loadDataSubject.compactMap { $0 }
compactMap
自身就是用来去除 nil
这和 Swift.Collection
功用一致。
# XxxxSubject
和 error
在运用 RxSwift
中一定要十分注意 不要 容易运用 Observer
的 .error(xxx)
,特别是在 flatMap
等 Operator
中。例如原文 ListViewModel
中:
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>
进行一次包裹,代码如下(存在省略写法):
let dynamycData = loadDataAction.filter { $0 != "0" }.map { cursor -> DynamicListParam in
DynamicListParam(cursor: cursor)
}.flatMap { param -> Observable<Result<XTListResultModel, Error>> in
let result = DynamicNetworkService.list(param: param.toJsonDict())
.request()
.map(XTListResultModel.self)
.map { model -> Result<XTListResultModel, Error> in
.success(model)
}.catch { .just(.failure($0)) }
return result.asObservable()
}
这样 loadDataSubject
在 RxMoya
呈现网络恳求、Model Decoder
等过错时也不会被停止。
# Model 的变动
现在 UI
界面需求展现不同类型的 cell
,因而我们需求对 DynamicListModel
进行一次包裹:
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
// 增加圈子数据恳求
private let topicListSubject = PublishSubject<Void>()
func loadFirstPageData() {
topicListSubject.onNext(())
loadDataSubject.onNext("0")
}
将 loadDataSubject.onNext("0")
替换为 loadFirstPageData()
。
在 func initializedNewDateSubject()
中增加对 topicListSubject
的 flatMap {}
操作
let topicListData = topicListSubject.flatMap { _ -> Observable<Result<TopicListModel, Error>> in
let result = DynamicNetworkService.topicListRecommend
.memoryCacheIn()
.request()
.map(TopicListModel.self)
.flatMap { model -> Single<Result<TopicListModel, Error>> in
.just(.success(model))
}.catch {
.just(.failure($0))
}
return result.asObservable()
}
然后对 topicListData
和 dynamycData
,进行 zip
操作:
let dynamycData = loadDataAction.filter { ... }
let topicListData = topicListSubject.flatMap { ... }
let newDataSubject = Observable.zip(dynamycData, topicListData).map { (dynamicWrapped, topicListWrapped) -> Result<DynamicDisplayModel, Error> 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
中修正以下代码:
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
办法。
# 修补
感谢某大佬的纠正,先修复 ViewModel
中运用了 subject(onNext:)
的过错, 代码以 refreshData
为例:
private func createNewDataSubject(with loadDataAction: Observable<String>) -> Observable<DynamicDisplayModel?> {
let dynamycData = ...
let topicListData = ...
let newDataSubject = Observable.zip(dynamycData, topicListData).map { [weak self] (dynamicWrapped, topicListWrapped) -> DynamicDisplayModel? in
var displayModel = DynamicDisplayModel()
switch dynamicWrapped {
case .success(let wrapped):
displayModel = DynamicDisplayModel.init(from: wrapped)
case .failure(let error):
// TODO: - 处理过错
if let error = error as? MoyaError {
self?.handleMoyaError(error, fromNewData: true)
} else {
print(error)
}
return nil
}
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)
}
defer {
self?.endRefreshDataSubject.onNext(())
// 清空当前恳求的状态
self?.loadDataSubject.onNext(nil)
}
return displayModel
}
return newDataSubject
}
回来 nweData
的当地修更改为:
private lazy var newDataObservable: Observable<DynamicDisplayModel> = {
let loadDataAction = loadDataSubject.compactMap { $0 }
let newData = self.createNewDataSubject(with: loadDataAction)
return newData.compactMap { $0 }
}()
var refreshData: Observable<DynamicDisplayModel> {
return self.newDataObservable
}
# 补充
-
关于网络层的封装可参看对沸点页面仿写的补充-网络层,当然现在的源码中已经包含了此部内容。
-
demo
最低版本要求iOS 13
这并不是Rx
等三方库导致,你完全可以调整到iOS 10
,并重新pod install
。运用iOS 13
仅仅是想在后续替换掉Rx
。