前言
通过上篇文章,我们了解到利用mvp可以对mvc的c层瘦身,并使得层次分明,且网络请求变得通用。那就已经很完美了,那么mvvm是用来干嘛的呢。
对于mvvm它是用来描述数据和视图的关系的。我们开发过程中常常发现数据变化会导致视图的变化,如一个列表,当没有数据的时候要展示空界面,当有数据的时候要展示对应条数的列表界面。在比如当网络请求回来的列表中的一个为已点赞数据,那么界面的相应视图就要显示红心其他都为灰心。
以上示例都反应了数据和视图的这种绑定关系,并且数据一变化视图就要相应的进行变化,也就是所谓的数据驱动。
以上介绍在说明数据驱动视图这件事,这和mvc,mvp是不同的,导致了编码思想也不同。
数据一变化视图就要变化,也就是要监听数据的变化,这可以利用kvo。但是苹果中的kvo用起来很麻烦,首先要注册kvo,并在界面销毁时候要释放kvo。而RAC的出现解决了这个麻烦,从而使得mvvm的写法变得更方便了。也就是说mvvm要用到kvo思想来监听数据源的变化,从而让视图变化。rac代替了kvo的这种繁琐写法。mvvm用的是kvo思想,rac不是必须的,但是用了更方便而已。关于rac的用发可以参考这里
MVVM
M:Model,它是模型类,就是有很多属性,每个属性名字和后台返回的字段名相同。
V:View,它是视图类,如继承与UIView类的所有类都可以认为是V层,并且Controller也属于V层,即继承与UIView,UIViewController类的所有类都属于V层。这个类里存放的都是和视图相关的逻辑代码。并且controller类里存放的是一些胶水代码(就是起到粘合作用的代码)。和p层有区别,加入了视图和数据源的绑定机制。
VM:viewModel,它是vm层,这里是主要写网络请求,数据逻辑的地方。处理和视图无关的数据逻辑等。这个类不该持有v层,也就是不该#import <UIKit/UIKit.h>。并且该类和p层是有区别的,它不是通过回调的方式返回到v层。而是通过kvo的方式。
三者关系是,v层持有vm层,vm层持有m层,并通过kvo的方式通知v层。
通过以上分析,其实mvvm设计模式和mvp很像。只不过是把p层中的网络请求的回调去掉了,通过kvo的方式返回到v层。并把视图和vm层进行绑定,将p层进行这两个修改后重新起一个名字叫做vm层,也就变成了mvvm了。
下边我们用一些伪代码来进一步探索MVVM的应用,其实和MVP一样,我们也需要封装出一套MVVM的基类。用这些基类可以使得我们编码更方便。
//ArticleListModel.h文件
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface ArticleListModel : NSObject
/** 文章内容 */
@property (copy, nonatomic) NSString *content;
/** 点赞否 */
@property (assign, nonatomic) NSInteger praise;
@end
NS_ASSUME_NONNULL_END
//ArticleListModel.m文件
#import "ArticleListModel.h"
@implementation ArticleListModel
@end
//ArticleListVC.h文件
#import "BSMvpUtilsBaseViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface ArticleListVC : BSMvpUtilsBaseViewController
@end
NS_ASSUME_NONNULL_END
//ArticleListVC.m文件
#import "ArticleListVC.h"
#import "ArticleListVM.h"
@interface ArticleListVC ()
@property (strong, nonatomic) ArticleListVM *vm;
@property (strong, nonatomic) UITableView *tableView;
@end
@implementation ArticleListVC
- (void)viewDidLoad {
[super viewDidLoad];
//v层持有vm层
self.vm = [[ArticleListVM alloc] init];
//初始化视图
[self setUpUI];
//视图与数据源绑定
[self bindView];
//网络请求
[self.vm fetchData];
}
- (void)setUpUI{
//tableView的布局等视图初始化代码
}
- (void)bindView{
@weakify(self);
[[RACObserve(self.vm, error) skip:1] subscribeNext:^(id _Nullable x) {
@strongify(self);
if (self.vm.error) {
//如果有错,弹出错误Toast等
}
}];
[[RACObserve(self.vm, arrList) skip:1] subscribeNext:^(id _Nullable x) {
@strongify(self);
if (self.vm.arrList.count == 0) {
//显示无数据占位图等
}else{
//隐藏无数据占位图等
}
//tableView刷新数据源
[self.tableView reloadData];
}];
}
//列表中的某一个点赞的心形被点击
- (void)cellPraiseBtnClick:(NSIndexPath *)indexPath{
//根据indexPath找到对应的数据源,然后根据数据源的状态请求相应的接口,最后改变数据源的状态。
[self.vm praiseMethod:indexPath.row];
}
@end
//ArticleListVM.h文件
#import "BSMvvmUtilsBaseVM.h"
@class ArticleListModel;
NS_ASSUME_NONNULL_BEGIN
@interface ArticleListVM : BSMvvmUtilsBaseVM
/** 出错 */
@property (strong, nonatomic) NSError *error;
/** 点赞列表数据源 */
@property (copy, nonatomic) NSArray<ArticleListModel *> *arrList;
- (void)fetchData;
/**
点赞和取消点赞方法
@param index index
*/
- (void)praiseMethod:(NSInteger)index;
/**
点赞,取消点赞请求
@param isPraise 1:点在 0:取消点赞
*/
- (void)fetchPraise:(NSNumber *)isPraise;
@end
NS_ASSUME_NONNULL_END
//ArticleListVM.m文件
#import "ArticleListVM.h"
#import "ArticleListModel.h"
#import <BSUtilsBaseRequestManager.h>
@implementation ArticleListVM
- (instancetype)init{
self = [super init];
if (self) {
_error = nil;
_arrList = nil;
}
return self;
}
//网络请求
- (void)fetchData{
[[BSUtilsBaseRequestManager new] postWithUrl:@"xxx" parmas:@{} finishBlk:^(NSDictionary * _Nonnull responseObject, NSError * _Nonnull error) {
if (error) {
self.error = error;
}else{
//数据转模型,并把转换的结果数据赋值给arrList
self.arrList = @[];
}
}];
}
//点赞和取消点赞方法
- (void)praiseMethod:(NSInteger)index{
NSMutableArray *arrTemp = [NSMutableArray arrayWithArray:self.arrList];
ArticleListModel *model = self.arrList[index];
NSInteger isPraise = model.praise;
if (isPraise) {
//当前是已经点过攒了
//下边要请求取消点赞接口,并判断是否请求成功
if(!self.error){
//取消点赞成功
model.praise = 0;
[arrTemp replaceObjectAtIndex:index withObject:model];
self.arrList = arrTemp;
}
}else{
//当前是还没有点过攒
//下边要请求点赞接口,并判断是否请求成功
if(!self.error){
//点赞成功
model.praise = 1;
[arrTemp replaceObjectAtIndex:index withObject:model];
self.arrList = arrTemp;
}
}
}
//点赞,取消点赞请求
- (void)fetchPraise:(NSNumber *)isPraise{
if(isPraise){
[[BSUtilsBaseRequestManager new] postWithUrl:@"xxx" parmas:@{@"praise":isPraise} finishBlk:^(NSDictionary * _Nonnull responseObject, NSError * _Nonnull error) {
self.error = error;
}];
}
}
@end
复制代码
总结
通过以上分析我们知道了mvvm是要把vm层和v层进行绑定的,然后我们只要改变vm层的数据源即可改变视图。这是关键。首先进行数据源和视图的绑定再改变数据源。
其实所谓的vm层和v层绑定其实就是监听vm层的数据源变化从而改变视图罢了。这种绑定利用RAC会更加方便。当然了不用RAC也可以的只是麻烦些而已。
总是要记住首先vm层和视图层的绑定,之后只要操作vm层的数据源即可。比如上述的代码我要显示列表界面,首先监听vm层的arrList,只要有变化就进入回调,在回调中判断是否要显示空界面还是具体的列表界面。这就是所谓的绑定。之后我们网络请求,如果请求成功就对arrList赋值,这个过程就是在改变vm层的数据源,从而使得视图更新。
在比如我们要对列表中的某个心形进行点击(这时候心形有两种状态的),进行点赞或者取消点赞。这时候我们根据index找到对应的数据源,进行网络请求,在把对应的数据源praise状态改变重新赋值给vm层的arrList属性即可,因为之前arrList数据源在和视图绑定着呢。所以这时候改变arrList从而更新了视图。
当你用了mvvm这种编码方式时候,可以感觉到减少了刷新代码,不必每次改变完数据后都要手动去写刷新视图的代码了。原因是绑定只要绑定一次即可,就在不停的监听数据的变化了。其实不仅仅是减少了刷新视图的代码,同时编码方式变了,思想变了。变成了数据驱动视图了。至于mvp好还是mvvm好,我个人觉得mvvm这种思想还是挺有趣的,同时这种数据驱动视图确实是普遍现象。而且这是一种新的有趣的编码方式。