抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

这是我参与更文挑战的第22天,活动详情查看: 更文挑战

什么是MVVM

其实对于MVVM是什么?有太多的文章与资料。绝对比我写的好,说的漂亮。所以我总结的不够专业和漂亮还请见谅。

作为一个iOS开发,其实站在原生开发角度上说,我是很少接触MVVM模式的,因为iOS的Cocoa框架是一个天然的MVC架构模式。所以我们先从MVC开始说吧。下面的这幅图广为流传:

59157bc07c7dd3399a8ce37413ff2553.jpeg

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!

b7ec292f5129b3cbc05a4c8c10e69e35.jpeg

既然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 = 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>.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() &#123;
    super.viewDidLoad()
    setupTableView()
&#125;

private func setupTableView() &#123;
    
    /// 设置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 &#123; _ in
            vm.refreshAction()
    &#125;.disposed(by: rx.disposeBag)
    
    /// 设置尾部刷新控件
    tableView.mj_footer = MJRefreshBackNormalFooter()
    
    tableView.mj_footer?.rx.refresh
        .subscribe &#123; _ in
            vm.loadMoreAction()
    &#125;.disposed(by: rx.disposeBag)
    
    /// 简单布局
    view.addSubview(tableView)
    tableView.snp.makeConstraints &#123; make in
        make.edges.equalTo(view)
    &#125;
    
    /// 数据源驱动
    vm.dataSource
        .asDriver(onErrorJustReturn: [])
        .drive(tableView.rx.items) &#123; (tableView, row, coinRank) in
        if let cell = tableView.dequeueReusableCell(withIdentifier: "Cell") &#123;
            cell.textLabel?.text = coinRank.username
            cell.detailTextLabel?.text = coinRank.coinCount?.toString
            return cell
        &#125;else &#123;
            let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell")
            cell.textLabel?.text = coinRank.username
            cell.detailTextLabel?.text = coinRank.coinCount?.toString
            return cell
        &#125;
    &#125;
    .disposed(by: rx.disposeBag)
    
    /// 下拉与上拉状态绑定到tableView
    vm.refreshSubject
        .bind(to: tableView.rx.refreshAction)
        .disposed(by: rx.disposeBag)
&#125;

}
复制代码

这样一来,是不是Controller中的代码更为精简与明了呢?

反馈下拉与上拉行为给vm,vm的dataSource去绑定tableView,vm中的refreshSubject去绑定tableView的下拉与上拉状态。

这就是所有的逻辑。

总结

我用了四天的更新,基本讲解了通过RxSwift构建一个页面的过程:

  • 分别用Swift和RxSwift编写同一个页面,使用Moya与RxMoya,表现其中的不同点。

  • 为页面中添加下拉刷新与上拉加载功能。

  • 为页面通过RxSwift封装MJRefresh,让编码更简洁,更Rx。

  • 在页面中抽离业务逻辑封装成ViewModel,并在页面中进行调用。

到此,一个页面的编码完成。

之前我就有说到过,玩安卓App的中的页面绝大部分都是列表,所以这四天的更新与知识点,对于很多页面都十分的通用。

后面我在讲解其他页面的时候,就不会在页面的基本网络请求、下拉与上拉方面做具体的分析了,也请各位知晓。

明日继续

就如上面总结说的,这个页面写完了,很多页面也都可以依葫芦画瓢了。

后续会对首页ViewModel、页面编写进行讲解。

大家加油!