本文介绍 MVVM 和 MVVM without Binding with DataController 这两种编程框架,主要以代码方式展现。发展历史和概念性问题可自行�查找哈。
MVVM
推荐阅读 被误解的 MVC 和被神化的 MVVM
Model-View-ViewModel
MVVM是为了实现Model层与View层的解耦。将ViewModel作为VM的�中介,在Model发生改变的时候,View也会自动变化。使用双向绑定技术,通常使用RAC(ReactiveCocoa)这个框架来完成。
该�例子主要使用RAC的方法是 RACObserve(TARGET, KEYPATH) 。TARGET是监听目标,KEYPATH是要观察的属性值,当属性值发生变化的时候会调用 subscribeNext:(void (^)(id x))nextBlock 这个方法,并将KEYPATH的最新属性值通过x传递。
实现目的:共有三个控制器,商品分类列表 SortListViewController --> 商品列表 GoodsListViewController --> 商品详情页 GoodsDetailViewController。功能为 商品分类列表 将分类ID传入 商品列表界面,使用分类ID请求该分类下的数据在 商品列表页面展示,点击商品列表将商品ID传入商品详情页。
SortListViewController
GoodsListModel *model = [[GoodsListModel alloc]init];
model.sortID = @"分类ID";
GoodsListViewModel *viewModel = [[GoodsListViewModel alloc]initWithGoodsListModel:model];
GoodsListViewController *goodsListVC = [[GoodsListViewController alloc]init];
goodsListVC.viewModel = viewModel;
[self.navigationController pushViewController:goodsListVC animated:YES];
此处的push传参是改变model.sortID 使GoodsListViewModel中的sortID监听发生改变,并保证每次传入不同的sortID都会执行方法。之后通过initWithGoodsListModel: 这个方法让GoodsListViewModel 进行数据请求,从而给ViewModel中的viewModelArray赋值,并将GoodsListViewModel与GoodsListViewController控制器绑定。
GoodsListViewController
商品列表这个控制器展现的为一个UICollectionView ,使用MVVM的框架修改,具体分为
GoodsListModel
GoodsListViewController
GoodsListViewModel
GoodsListView
GoodsListCellViewModel
GoodsListCell
- GoodsListModel
GoodsListViewModel.h
#import <Foundation/Foundation.h>
@interface GoodsListModel : NSObject
@property (strong ,nonatomic) NSString *sortID; //分类ID
@property (strong ,nonatomic) NSString *goodsID; //商品ID
@property (strong ,nonatomic) NSString *goodsName; //商品名称
@property (strong ,nonatomic) NSString *goodsPrice; //商品价格
@end
这里解释下sortID, 商品列表GoodsListViewController这个控制器只是需要sortID来进行数据请求,并不要把这个sortID写入该控制器的Model类中。之所以写入,是便于每次传入sortID 而进行的数据请求,仅仅是为了方便起见,在GoodsListViewModel中网络请求完成的字典转模型时并不需要该参数。
也可以使用商品分类列表SortListViewController控制器的model类,在传入GoodsListViewModel 和 GoodsListViewModel 监听的model 做出修改即可。
- GoodsListViewController
GoodsListViewController.h
#import <UIKit/UIKit.h>
@class GoodsListViewModel;
@interface GoodsListViewController : UIViewController
@property (strong ,nonatomic) GoodsListViewModel *viewModel;
@end
GoodsListViewController.m
#import "GoodsListViewController.h"
#import "GoodsListViewModel.h"
#import "GoodsListView.h"
#import "GoodsDetailViewController.h"
@interface GoodsListViewController ()
@property (strong ,nonatomic) GoodsListView *goodsListView;
@end
@implementation GoodsListViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.goodsListView]
[RACObserve(self.viewModel, viewModelArray) subscribeNext:^(id x) {
self.goodsListView.viewModel = self.viewModel;
}];
[[self.viewModel.cellClickSubject takeUntil:self.rac_willDeallocSignal]subscribeNext:^(id x) {
GoodsDetailViewController *goodsDetailsVC = [[GoodsDetailViewController alloc]init];
goodsDetailsVC.goodsID = x;
[self.navigationController pushViewController:goodsDetailsVC animated:YES];
}];
}
- (GoodsListView *)goodsListView{
if (!_goodsListView) {
_goodsListView = [[GoodsListView alloc]initWithFrame:CGRectMake(0, 0, kScreenW, kScreenH)];
}
return _goodsListView;
}
@end
商品列表控制器,在上个控制器push进来的时候已经将GoodsListViewModel与GoodsListViewController绑定。
RACObserve这个方法是监听GoodsListViewModel 中的viewModelArray (viewModelArray可以看做是网络请求字典转模型之后的模型数组)。当viewModelArray改变时将新的ViewModel传递给GoodsListView。实现了GoodsListViewController 与 goodsListView的绑定。这样双向绑定就完成了。
goodsListView 可以看做子视图的集合View,控制器仅仅需要在数据发生改变的时候告诉goodsListView即可,不需要管理数据的内容和数据的展示方式。
- GoodsListViewModel
GoodsListViewModel.h
#import <Foundation/Foundation.h>
@class GoodsListCellViewModel;
@class GoodsListModel;
@interface GoodsListViewModel : NSObject
@property (copy, nonatomic) NSArray * viewModelArray;
@property (nonatomic, strong) RACSubject *cellClickSubject;
- (instancetype)initWithGoodsListModel: (GoodsListModel *) model;
- (void)first;
- (void)next;
- (GoodsListCellViewModel *)itemViewModelForIndex:(NSInteger)index;
@end
GoodsListViewModel.m
#import "GoodsListViewModel.h"
#import "GoodsListModel.h"
#import "GoodsListCellViewModel.h"
@interface GoodsListViewModel ()
@property (strong ,nonatomic) NSString *sortID;
@property (strong, nonatomic) NSNumber *nextPageNumber;
@property (strong, nonatomic)NSDictionary *paramsDic;
@end
@implementation GoodsListViewModel
- (instancetype)initWithGoodsListModel:(GoodsListModel *)model{
self = [super init];
if (nil != self) {
[RACObserve(model, sortID) subscribeNext:^(NSString *sort) {
self.sortID = sort;
self.nextPageNumber = @1;
}];
[self requestInfo];
}
return self;
}
- (void)requestInfo{
[RACObserve(self, nextPageNumber) subscribeNext:^(NSNumber * nextPageNumber) {
NSDictionary *params = @{@"id":self.sortID,
@"page":nextPageNumber,
@"num":@15,};
self.paramsDic = params;
}];
[[RACObserve(self, paramsDic) filter:^BOOL(id value) {
return value;
}] subscribeNext:^(NSDictionary * params) {
//网络请求并给_viewModelArray赋值
//RAC 的网络请求
[[self.httpClient rac_POST:@"URL" parameters:params] subscribeNext:^(RACTuple *JSONAndHeaders) {
JSONAndHeaders.first 为请求内容
}];
}];
}
- (void)first{
self.nextPageNumber = @1;
}
- (void)next{
self.nextPageNumber = [NSNumber numberWithInteger: [self.nextPageNumber integerValue] + 1];
}
- (GoodsListCellViewModel *)itemViewModelForIndex:(NSInteger)index{
return [[GoodsListCellViewModel alloc]initWithModel:[_viewModelArray objectAtIndex:index]];
}
- (RACSubject *)cellClickSubject {
if (!_cellClickSubject) {
_cellClickSubject = [RACSubject subject];
}
return _cellClickSubject;
}
@end
ViewModel核心代码。可以看出nextPageNumber页码参数,paramsDic都是被观察的对象,每当新的sortID传入时,nextPageNumber都会重置为1,从而影响paramsDic,进而走网络请求。
initWithModel 方法将模型数组_viewModelArray 转为 GoodsListCellViewModel中的参数。
itemViewModelForIndex 方法在GoodsListView中给cell绑定数据的时候调用。
first 方法用于下拉刷新,将nextPageNumber重置为1。
next 方法用于上拉加载,将nextPageNumber +1。
- GoodsListView
GoodsListView.h
#import <UIKit/UIKit.h>
@class GoodsListViewModel;
@interface GoodsListView : UIView
@property (nonatomic, strong) GoodsListViewModel *viewModel;
- (instancetype)initWithFrame:(CGRect)frame;
@end
GoodsListView.m
#import "GoodsListView.h"
#import "GoodsListViewModel.h"
#import "GoodsListCell.h"
@interface GoodsListView ()
@property (strong ,nonatomic) UICollectionView *collectionView;
@end
@implementation GoodsListView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
[self setupView];
[RACObserve(self, self.viewModel) subscribeNext:^(id x) {
[_collectionView reloadData];
}];
}
return self;
}
- (void)setupView{
//_collectionView 添加
_collectionView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
[self.viewModel first];
}];
_collectionView.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingBlock:^{
[self.viewModel next];
}];
}
//此处省去�UICollectionView的代理
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
GoodsListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"cellIdentifier" forIndexPath:indexPath];
cell.cellViewModel = [self.viewModel itemViewModelForIndex:indexPath.row];
return cell;
}
-(void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
[self.viewModel.cellClickSubject sendNext:@"点击商品的ID"];
}
@end
子视图的集合,继承于UIView。这里仅放置了一个UICollectionView,添加了上拉加载,下拉刷新的功能,省去了_collectionView的创建和代理的设置。在初始化的时候将self.viewModel作为被观察的对象,当商品列表控制器传递过来的viewModel改变时将会调用UICollectionView的刷新方法。
itemViewModelForIndex:indexPath 这个方法是给cell绑定cellViewModel的数据,其实这里也可以省略,直接将viewModel的viewModelArray直接传给cell,省去了GoodsListCellViewModel的创建。
- GoodsListCellViewModel
GoodsListCellViewModel.h
#import <Foundation/Foundation.h>
@class GoodsListModel;
@interface GoodsListCellViewModel : NSObject
- (instancetype)initWithModel:(GoodsListModel *)model;
@property (strong ,nonatomic) NSString *goodsID;
@property (strong ,nonatomic) NSString *goodsName;
@property (strong ,nonatomic) NSString *goodsPrice;
@end
GoodsListCellViewModel.m
#import "GoodsListCellViewModel.h"
#import "GoodsListModel.h"
@interface GoodsListCellViewModel ()
@property (strong ,nonatomic) GoodsListModel *model;
@end
@implementation GoodsListCellViewModel
- (instancetype)initWithModel:(GoodsListModel *)model{
self = [super init];
if (self) {
self.model = model;
}
return self;
}
- (NSString *)goodsID{
return self.model.goodsID;
}
- (NSString *)goodsName{
return self.model.goodsName;
}
- (NSString *)goodsPrice{
return self.model.goodsPrice;
}
@end
将viewModel中viewModelArray的模型转为 GoodsListCellViewModel
- GoodsListCell
GoodsListCell.h
#import <UIKit/UIKit.h>
@class GoodsListCellViewModel;
@interface GoodsListCell : UICollectionViewCell
@property (strong ,nonatomic) GoodsListCellViewModel *cellViewModel;
@end
GoodsListCell.m
#import "GoodsListCell.h"
#import "GoodsListCellViewModel.h"
@implementation GoodsListCell
//Cell的布局
- (void)setCellViewModel:(GoodsListCellViewModel *)cellViewModel{
_cellViewModel = cellViewModel;
//cell控件的赋值
}
@end
使用GoodsListCellViewModel进行赋值即可
MVVM without Binding with DataController
控制器通过 DataController 请求数据,并将数据装配给ViewModel,ViewModel将数据中的模型转为View 对应的ViewModel ,View负责展示UI,并将事件传递给控制器。
本例子为商品列表页,默认子视图为CollectionView。分为
GoodsListViewController
GoodsListModel
GoodsListDataController
GoodsListViewModel
GoodsListCellViewModel
GoodsListView
GoodsListCell
- GoodsListViewController
GoodsListViewController.m
#import "GoodsListViewController.h"
#import "GoodsListDataController.h"
#import "GoodsListView.h"
#import "GoodsListViewModel.h"
@interface GoodsListViewController ()<GoodsListViewDelegate>
@property (strong ,nonatomic) GoodsListDataController *dataManager;
@property (strong ,nonatomic) GoodsListView *goodsListView;
@end
@implementation GoodsListViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"商品列表";
[self.view addSubview:self.goodsListView];
[self fectchGoodsModelData];
}
- (void)fectchGoodsModelData{
[self.dataManager requestGoodsModelDataWhenSucess:^{
if (self.dataManager.goodsListModelArray.count > 0) {
[self renderGoodsListView];
}
} fail:^(NSError *error) {
}];
}
- (void)renderGoodsListView{
GoodsListViewModel *viewModel = [GoodsListViewModel viewModelWithGoodsModel:self.dataManager.goodsListModelArray];
[self.goodsListView bindDataWithViewModel:viewModel];
}
- (void)didSelectedRowOfGoodsID:(NSString *)goodsID{
//子视图传递事件的代理
}
- (GoodsListDataController *)dataManager{
if (!_dataManager) {
_dataManager = [[GoodsListDataController alloc]init];
}
return _dataManager;
}
- (GoodsListView *)goodsListView{
if (!_goodsListView) {
_goodsListView = [[GoodsListView alloc]initWithFrame:CGRectMake(0, 0, kScreenW, kScreenH)];
_goodsListView.delegate = self;
}
return _goodsListView;
}
@end
商品列表控制器。添加子视图集合GoodsListView,初始化GoodsListDataController。
fectchGoodsModelData 方法使dataManager请求�网络请求,成功之后调用 renderGoodsListView 方法将数据装配到 ViewModel 中,再将ViewModel传递至 GoodsListView,更新数据刷新UI。 GoodsListView 中的事件通过代理传递到控制器中。
- GoodsListModel
GoodsListModel.h
#import <Foundation/Foundation.h>
@interface GoodsListModel : NSObject
@property (strong ,nonatomic) NSString *goodsID;
@property (strong ,nonatomic) NSString *goodsName;
@property (strong ,nonatomic) NSString *goodsPrice;
@end
简单的商品属性
- GoodsListDataController
GoodsListDataController.h
#import <Foundation/Foundation.h>
@class GoodsListModel;
@interface GoodsListDataController : NSObject
@property (strong ,nonatomic) NSMutableArray<GoodsListModel *> *goodsListModelArray;
- (void)requestGoodsModelDataWhenSucess:(void (^)())sucess fail:(void(^)(NSError *))fail;
@end
GoodsListDataController.m
#import "GoodsListDataController.h"
#import "GoodsListModel.h"
@implementation GoodsListDataController
- (instancetype)init{
self = [super init];
if (self) {
self.goodsListModelArray = [[NSMutableArray alloc]init];
}
return self;
}
- (void)requestGoodsModelDataWhenSucess:(void (^)())sucess fail:(void (^)(NSError *))fail{
self.goodsListModelArray = 模型数组
}
@end
DataController负责数据请求,并将模型数组赋值给self.goodsListModelArray。DataController数据的获取和处理从ViewModel抽离,一定程度上简化了ViewModel的工作量,当DataController收到外界ViewController的请求后进行数据处理,进而更新UI。这样ViewController不需要关心数据的获取和处理,DataController不需要关心数据的展示和交互。
- GoodsListViewModel
GoodsListViewModel.h
#import <Foundation/Foundation.h>
@class GoodsListModel;
@class GoodsListCellViewModel;
@interface GoodsListViewModel : NSObject
@property (strong ,nonatomic) NSArray <GoodsListCellViewModel *> *cellViewModelArray;
+ (GoodsListViewModel *)viewModelWithGoodsModel:(NSArray <GoodsListModel *>*)goodsListModelArray;
@end
GoodsListViewModel.m
#import "GoodsListViewModel.h"
#import "GoodsListModel.h"
#import "GoodsListCellViewModel.h"
@implementation GoodsListViewModel
+ (GoodsListViewModel *)viewModelWithGoodsModel:(NSArray <GoodsListModel *>*)goodsListModelArray{
GoodsListViewModel *viewModel = [[GoodsListViewModel alloc]init];
NSMutableArray *cellViewModelArray = [[NSMutableArray alloc]init];
for (GoodsListModel *goodsListModel in goodsListModelArray) {
GoodsListCellViewModel *cellViewModel = [GoodsListCellViewModel viewModelWithGoodsListModel:goodsListModel];
[cellViewModelArray addObject:cellViewModel];
}
viewModel.cellViewModelArray = cellViewModelArray;
return viewModel;
}
@end
ViewModel,将数据装配到viewModel。此处使用工厂方法创建一个带有cellViewModelArray属性的GoodsListViewModel对象。具体是将网络请求之后的goodsListModelArray模型数组转为 GoodsListCellViewModel类型的数组,并将这种类型的数组作为ViewModel的一个属性。
- GoodsListCellViewModel
GoodsListCellViewModel.h
#import <Foundation/Foundation.h>
@class GoodsListModel;
@interface GoodsListCellViewModel : NSObject
@property (strong ,nonatomic) NSString *goodsID;
@property (strong ,nonatomic) NSString *goodsName;
@property (strong ,nonatomic) NSString *goodsPrice;
+ (GoodsListCellViewModel *)viewModelWithGoodsListModel:(GoodsListModel *)goodsListModel;
@end
GoodsListCellViewModel.m
#import "GoodsListCellViewModel.h"
#import "GoodsListModel.h"
@implementation GoodsListCellViewModel
+ (GoodsListCellViewModel *)viewModelWithGoodsListModel:(GoodsListModel *)goodsListModel{
GoodsListCellViewModel *cellViewModel = [[GoodsListCellViewModel alloc]init];
cellViewModel.goodsID = goodsListModel.goodsID;
cellViewModel.goodsName = goodsListModel.goodsName;
cellViewModel.goodsPrice = goodsListModel.goodsPrice;
return cellViewModel;
}
@end
将viewModel中goodsListModelArray的模型转为 GoodsListCellViewModel
- GoodsListView
GoodsListView.h
#import <UIKit/UIKit.h>
@class GoodsListViewModel;
@protocol GoodsListViewDelegate <NSObject>
- (void)didSelectedRowOfGoodsID:(NSString *)goodsID;
@end
@interface GoodsListView : UIView
@property (weak, nonatomic) id<GoodsListViewDelegate> delegate;
- (void)bindDataWithViewModel:(GoodsListViewModel *)viewModel;
@end
GoodsListView.m
#import "GoodsListView.h"
#import "GoodsListViewModel.h"
#import "GoodsListCell.h"
#import "GoodsListCellViewModel.h"
@interface GoodsListView ()
@property (strong ,nonatomic) GoodsListViewModel *viewModel;
@property (strong ,nonatomic) UICollectionView *collectionView;
@end
@implementation GoodsListView
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
//子视图 初始化,布局
}
return self;
}
- (void)bindDataWithViewModel:(GoodsListViewModel *)viewModel{
self.viewModel = viewModel;
[self.collectionView reloadData];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.viewModel.cellViewModelArray.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
GoodsListCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"GoodsListCell" forIndexPath:indexPath];
GoodsListCellViewModel *cellViewModel = self.viewModel.cellViewModelArray[indexPath.row];
[cell bindGoodsListCellDataWithCellViewModel:cellViewModel];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
GoodsListCellViewModel *cellViewModel = self.viewModel.cellViewModelArray[indexPath.row];
if ([self.delegate respondsToSelector:@selector(didSelectedRowOfGoodsID:)]) {
[self.delegate didSelectedRowOfGoodsID:cellViewModel.goodsID];
}
}
@end
GoodsListView,子视图的集合。当数据装配至ViewModel时,通过bindDataWithViewModel: 方法将带有cellViewModelArray属性的ViewModel传递至子视图,使子视图刷新UI。bindGoodsListCellDataWithCellViewModel: 方法为Cell绑定数据,collectionView的点击方法通过代理传递给控制器。
- GoodsListCell
GoodsListCell.h
#import <UIKit/UIKit.h>
@class GoodsListCellViewModel;
@interface GoodsListCell : UICollectionViewCell
- (void)bindGoodsListCellDataWithCellViewModel:(GoodsListCellViewModel *)cellViewModel;
@end
GoodsListCell.m
#import "GoodsListCell.h"
#import "GoodsListCellViewModel.h"
@implementation GoodsListCell
- (void)bindGoodsListCellDataWithCellViewModel:(GoodsListCellViewModel *)cellViewModel{
//控件赋值
}
@end
将单个的cellViewModel值传递到Cell,并给Cell中的控件赋值。
结语
MVVM 使用的是双向绑定技术,需要RAC的支持,学习成本较大。
MVVM without Binding with DataController 是通过数据的装配和代理的方法实现了解耦。�并且将ViewModel中的网络请求给单独�抽离出来作为DataController,使ViewModel的结构更加简洁。理解较为容易。
MVC 简单经典的�架构,如果把基类写的足够便利,简单的页面还是推荐使用。
需要依据项目的开发和维护来选择合适的架构,有利有弊。
这是几个月前技术分享的内容,PPT上显示的是16/6/28,貌似好久了。当时用项目中的代码,把大家讲的可晕。现在把思路重写理了一遍,发现好多了,就记录下来,以后看着也方便。