这是我参与更文挑战的第22天,活动详情查看: 更文挑战
什么是MVVM
其实对于MVVM是什么?有太多的文章与资料。绝对比我写的好,说的漂亮。所以我总结的不够专业和漂亮还请见谅。
作为一个iOS开发,其实站在原生开发角度上说,我是很少接触MVVM模式的,因为iOS的Cocoa框架是一个天然的MVC架构模式。所以我们先从MVC开始说吧。下面的这幅图广为流传:
M即Model,专门用来表示业务数据。
V即UIView,专门用来做页面展示。
C即UIViewController,用来处理接受到的交互事件,并进行处理后,想改变后的数据Model传递给View,更新页面等。
其实在iOS开发看来,嗯,一切正常,没毛病!然后就会在Controller里面写UI,写网络请求…..然后,在OC时代,如果没有良好的整理与封装,一个Controller破千行是件容易的事情。写代码的人一时爽,看代码的人千行泪。
其实我们往往陷入了这样一个误区,UIViewController它是一个Controller,但是看看它的前缀UIView/Controller,它同时承载的作为UIView的使命啊,一人多职,不臃肿才怪呢!
甚至在最日常的代码中的页面跳转中,比如在某个view页面,点击了需要push到下一个页面,我们必须把view的事件回调到Controller层,然后通过Controller去push(当然这是iOS设计如此)。
但是在其他的开发中(Vue和Flutter),根本就没有所谓的Controller,任意一个页面的事件都可以做跳转。
所以狭隘的看问题,我们总是活生生的把UIView和UIViewController给分开了,明明它们就是一家人呀!所以可以说UIViewController是被处理数据和逻辑而被耽误的UIView!
既然UIViewController不适合做数据处理和逻辑,那么我们就做这样一个层去干这个事吧,于是MVVM就出现了:
Model <=> ViewModel <=> UIView/UIViewController
UIView/UIViewController的作用仅仅是介绍交互事件与展示数据。
ViewModel接受由UIView/UIViewController传递过来的事件,并做Model数据和逻辑业务,然后将处理好的数据由给UIView/UIViewController,进而去驱动UIView/UIViewController视图的变化。
Model还是那个Model,用来表示业务数据。
重新分割职能后,我们看事情的角度和方向就有了新的变化。
说把了MVVM,在iOS中就是把UIView/UIViewController都看做是View层,通过新建ViewModel这一层,去处理之前Controller干的事情,由于是通过数据绑定去驱动页面的,所以交互->页面变化,自然而然。
为什么是MVVM
我们叫ViewModel层,完全是出于习惯,你把它当做是一个中间层就可以,命名嘛,只不过是大家都这么叫于是就这么一直叫了。
MVVM其实已经在开发中大面积使用,特别是前端,基本上主流的框架的都是MVVM模式,同时它也经受住了考验,证明了这种模式的优越。
只是一般iOS开发中,原生对于数据绑定与驱动鲜有良好的支持,所以使得MVVM这种模式施展不开拳脚。而RxSwift却又恰恰是为MVVM模式而生的!
这里有一篇大佬写的通过原生支持MVVM的文章,大家可以看一看,原生是多么的难——MVC和MVVM详解。
编写和使用ViewModel
编写:抽离数据与业务逻辑
我们新建一个类,叫RxSwiftCoinRankListViewModel,来进行抽离与封装:
class RxSwiftCoinRankListViewModel {
/// 初始化page为1
private var page: Int = 1
/// DisposeBag
private let disposeBag: DisposeBag
/// 既是可监听序列也是观察者的数据源,里面封装的其实是BehaviorSubject
let dataSource: BehaviorRelay<[CoinRank]> = BehaviorRelay(value: [])
/// 既是可监听序列也是观察者的状态枚举
let refreshSubject: BehaviorSubject<MJRefreshAction> = BehaviorSubject(value: .begainRefresh)
/// 初始化方法
/// - Parameter disposeBag: 传入的disposeBag
init(disposeBag: DisposeBag) {
self.disposeBag = disposeBag
}
/// 下拉刷新行为
func refreshAction() {
resetCurrentPageAndMjFooter()
getCoinRank(page: page)
}
/// 上拉加载更多行为
func loadMoreAction() {
page = page + 1
getCoinRank(page: page)
}
/// 下拉的参数与状态重置行为
private func resetCurrentPageAndMjFooter() {
page = 1
refreshSubject.onNext(.resetNomoreData)
}
/// 网络请求
private func getCoinRank(page: Int) {
myProvider.rx.request(MyService.coinRank(page))
/// 转Model
.map(BaseModel<Page<CoinRank>>.self)
/// 由于需要使用Page,所以return到$0.data这一层,而不是$0.data.datas
.map{ $0.data }
/// 解包
.compactMap { $0 }
/// 转换操作
.asObservable()
.asSingle()
/// 订阅
.subscribe { event in
/// 订阅事件
/// 通过page的值判断是下拉还是上拉(可以用枚举),不管成功还是失败都结束刷新状态
page == 1 ? self.refreshSubject.onNext(.stopRefresh) : self.refreshSubject.onNext(.stopLoadmore)
switch event {
case .success(let pageModel):
/// 解包数据
if let datas = pageModel.datas {
/// 通过page的值判断是下拉还是上拉,做数据处理,这里为了方便写注释,没有使用三目运算符
if page == 1 {
/// 下拉做赋值运算
self.dataSource.accept(datas)
}else {
/// 上拉做合并运算
self.dataSource.accept(self.dataSource.value + datas)
}
}
/// 解包curPage与pageCount
if let curPage = pageModel.curPage, let pageCount = pageModel.pageCount {
/// 如果发现它们相等,说明是最后一个,改变foot而状态
if curPage == pageCount {
self.refreshSubject.onNext(.showNomoreData)
}
}
case .error(_):
/// error占时不做处理
break
}
}.disposed(by: disposeBag)
}
}
复制代码
使用
import UIKit
import RxSwift
import RxCocoa
import NSObject_Rx
import MJRefresh
class RxSwiftCoinRankListController: BaseViewController {
/// 懒加载tableView
private lazy var tableView = UITableView(frame: .zero, style: .plain)
override func viewDidLoad() {
super.viewDidLoad()
setupTableView()
}
private func setupTableView() {
/// 设置tableFooterView
tableView.tableFooterView = UIView()
/// 设置代理
tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
/// 创建vm
let vm = RxSwiftCoinRankListViewModel(disposeBag: rx.disposeBag)
/// 设置头部刷新控件
tableView.mj_header = MJRefreshNormalHeader()
tableView.mj_header?.rx.refresh
.subscribe { _ in
vm.refreshAction()
}.disposed(by: rx.disposeBag)
/// 设置尾部刷新控件
tableView.mj_footer = MJRefreshBackNormalFooter()
tableView.mj_footer?.rx.refresh
.subscribe { _ in
vm.loadMoreAction()
}.disposed(by: rx.disposeBag)
/// 简单布局
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.edges.equalTo(view)
}
/// 数据源驱动
vm.dataSource
.asDriver(onErrorJustReturn: [])
.drive(tableView.rx.items) { (tableView, row, coinRank) in
if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") {
cell.textLabel?.text = coinRank.username
cell.detailTextLabel?.text = coinRank.coinCount?.toString
return cell
}else {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
cell.textLabel?.text = coinRank.username
cell.detailTextLabel?.text = coinRank.coinCount?.toString
return cell
}
}
.disposed(by: rx.disposeBag)
/// 下拉与上拉状态绑定到tableView
vm.refreshSubject
.bind(to: tableView.rx.refreshAction)
.disposed(by: rx.disposeBag)
}
}
复制代码
这样一来,是不是Controller中的代码更为精简与明了呢?
反馈下拉与上拉行为给vm,vm的dataSource去绑定tableView,vm中的refreshSubject去绑定tableView的下拉与上拉状态。
这就是所有的逻辑。
总结
我用了四天的更新,基本讲解了通过RxSwift构建一个页面的过程:
-
分别用Swift和RxSwift编写同一个页面,使用Moya与RxMoya,表现其中的不同点。
-
为页面中添加下拉刷新与上拉加载功能。
-
为页面通过RxSwift封装MJRefresh,让编码更简洁,更Rx。
-
在页面中抽离业务逻辑封装成ViewModel,并在页面中进行调用。
到此,一个页面的编码完成。
之前我就有说到过,玩安卓App的中的页面绝大部分都是列表,所以这四天的更新与知识点,对于很多页面都十分的通用。
后面我在讲解其他页面的时候,就不会在页面的基本网络请求、下拉与上拉方面做具体的分析了,也请各位知晓。
明日继续
就如上面总结说的,这个页面写完了,很多页面也都可以依葫芦画瓢了。
后续会对首页ViewModel、页面编写进行讲解。
大家加油!