阅读 2247

Swift 开发 wanandroid 客户端——封装ViewModel层

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

什么是MVVM

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

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

59157bc07c7dd3399a8ce37413ff2553.jpeg

M即Model,专门用来表示业务数据。

V在iOS中即UIView,专门用来做页面展示。

C在iOS中即UIViewController,用来处理接受到的交互事件,并进行处理后,想改变后的数据Model传递给View,更新页面等。

其实在iOS开发看来,嗯,一切正常,没毛病!

然后就会在Controller里面写UI,写网络请求.....然后,在OC时代,如果没有良好的整理与封装,一个Controller破千行是件容易的事情。写代码一时爽,CodeReview千行泪。

其实我们往往陷入了这样一个误区,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<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、页面编写进行讲解。

大家加油!

文章分类
iOS
文章标签