前言 MVVMDemo
之前几个月一直在学习react-native,它的组件化开发真的是很棒,控件和页面的组件化在开发中可以很好的复用,节省开发时间。在那个时候还不知道react-native开发用到的就是MVVM设计模式。
前几天,UI给了新的需求,需要添加几个页面(之前的项目一直使用MVC开发的),在给这几个新页面添加入口的时候,感觉之前写的代码真的是好恶心😱😱😱,就在网上搜了搜MVP和MVVM,发现MVVM和我在写RN时的写法很像。就研究了一下,然后写下了这篇文章。(可能会有很多问题,欢迎评论)
ps:这篇文章实用为主,那些理论性的东西,我都没有研究。
俗话说得好:黑猫白猫,能用在项目中的就是好🐱
更新
解决了,之前存在的button需要在HeaderView中复写的问题。
吐槽
之前在网上搜索MVVM设计模式的时候,很多文章都说到MVVM和ReactiveCocoa最好是搭配在一起使用,这样效率更高。我承认ReactiveCocoa是个好东西,但我为什么没有研究,而只写了这个简单的MVVMDemo呢?因为我觉得,虽然直接使用MVVM会有数据绑定方面的问题,但如果为了使用MVVM还要在项目中集成ReactiveCocoa,对团队开发都不是很友好的,而我本身也只是希望将项目中的一些类拆分,用简单的MVVM就足够了,这样可能省下大量的时间成本去做别的事情。
当然,以后如果有机会,我也可能会将项目使用MVVM和ReactiveCocoa来重构。
使用
MVVM顾名思义,那就是Model,View,ViewModel,所以我们需要创建这些类了。
项目目录.png
接下来就把我的理解说说。
ViewModelClass
ViewModelClass.pngViewModelClass.h
中
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
//定义返回请求数据的block类型
// 成功返回的数据
typedef void (^ReturnValueBlock) (id returnValue);
// 失败返回的数据
typedef void (^ErrorCodeBlock) (id errorCode);
@interface ViewModelClass : NSObject
@property (strong, nonatomic) ReturnValueBlock returnBlock;
@property (strong, nonatomic) ErrorCodeBlock errorBlock;
// 传入交互的Block块
-(void) setBlockWithReturnBlock: (ReturnValueBlock) returnBlock
WithErrorBlock: (ErrorCodeBlock) errorBlock;
@end
ViewModelClass.m
#import "ViewModelClass.h"
#import "RTNetworking.h"
@implementation ViewModelClass
#pragma 接收传过来的block
-(void) setBlockWithReturnBlock: (ReturnValueBlock) returnBlock
WithErrorBlock: (ErrorCodeBlock) errorBlock
{
_returnBlock = returnBlock;
_errorBlock = errorBlock;
}
@end
上面的两个类放着的就是ViewModel的基类,用下面的方法承接之后继承于这个基类的VM中的回调数据。
- (void)setBlockWithReturnBlock: (ReturnValueBlock) returnBlock
WithErrorBlock: (ErrorCodeBlock) errorBlock;
ViewController
我之前的项目用的MVC设计模式,C指的就是这个ViewController了,之前写的垃圾代码,一个Controller里面放过1000多行代码,现在去找个方法需要N久。但虽然这是个简单的例子,但真正利用之后并不简单。
ViewController.m
// 初始化HeaderVM
HeaderVM *headerView = [[HeaderVM alloc] init];
// 初始化HomeVM
HomeVM *model = [[HomeVM alloc]init];
// 调用ViewModelClass基类的方法,来获取数据
[model setBlockWithReturnBlock:^(id returnValue) {
_dataArray = returnValue;
_listArray = returnValue[@"picList"];
_categoryArray = returnValue[@"category_new"];
UIView *view = [headerView headerViewWithData:_categoryArray];
self.tableView.tableHeaderView = view;
[self.tableView reloadData];
} WithErrorBlock:^(id errorCode) {
NSLog(@"%@",errorCode);
}];
上面的代码虽短,但最重要的东西都在里面,通过Block回调,将需要的数据在VM页面回传了回来。具体内容见MVVMDemo
HomeVM
HomeVM.pngHomeVM.h
#import "ViewModelClass.h"
@interface HomeVM : ViewModelClass
// 获取商品列表
- (void)fetchShopList;
// 跳转到商品详情页
- (void)shopListDetailWithVC:(UIViewController *)vc didSelectRowAtDic:(NSDictionary *)dic;
@end
HomeVM.m
#import "HomeVM.h"
#import "RTNetworking.h"
#import "DetailViewController.h"
@implementation HomeVM
- (void)fetchShopList{
[RTNetworking getWithUrl:@"/v1/Home/all.json" refreshCache:NO success:^(id response) {
[self loadDataWithSuccessDic:response];
} fail:^(NSError *error) {
self.errorBlock(error);
}];
}
- (void)loadDataWithSuccessDic:(NSDictionary *)dic{
NSMutableArray *arr = dic[@"data"];
self.returnBlock(arr);
}
- (void)shopListDetailWithVC:(UIViewController *)vc didSelectRowAtDic:(NSDictionary *)dic{
DetailViewController *view = [[DetailViewController alloc]init];
view.labelText = dic[@"title"];
[vc.navigationController pushViewController:view animated:YES];
}
@end
可以明显看出HomeVM
是继承于ViewModelClass
,在这个VM中,将Push到新页面的方法也写在了里面。
HeaderView
这个是tableView的headerView。
HeaderView.pngHeaderView.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef void(^HeaderViewBlock)(NSString *shopId);
@interface HeaderView : UIView
@property (nonatomic, copy) HeaderViewBlock block;
// headerView中的数据
- (void)headerViewWithData:(id)data;
- (void)setBlock:(HeaderViewBlock)block;
@end
HeaderView.m
#import "HeaderView.h"
#import "UIKit+AFNetworking.h"
#define SCREEN_WIDTH [UIScreen mainScreen].bounds.size.width
@implementation HeaderView{
UIImageView *topImage;
NSMutableArray *dataArray;
UIButton *button;
}
- (void)headerViewWithData:(id)data{
dataArray = data;
CGFloat btnWidth = SCREEN_WIDTH * 0.17;
CGFloat btnHeight = SCREEN_WIDTH * 0.17;
CGFloat margin=(SCREEN_WIDTH-5*btnWidth)/6;
for (int i = 0; i <dataArray.count; i++) {
int row = i % 5;
int loc = i / 5;
CGFloat appviewx=margin+(margin+btnWidth)*row;
CGFloat appviewy=5 + (10+btnHeight)*loc;
button = [UIButton buttonWithType:(UIButtonTypeCustom)];
button.frame = CGRectMake(appviewx, appviewy, btnWidth, btnHeight);
button.highlighted = NO;
button.tag = [dataArray[i][@"id"] integerValue];
[button setImageForState:(UIControlStateNormal) withURL:[NSURL URLWithString:dataArray[i][@"icon"]]];
button.userInteractionEnabled = YES;
[button addTarget:self action:@selector(button:) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:button];
}
}
- (void)button:(UIButton *)btn{
NSString *shopId = [NSString stringWithFormat:@"%ld",(long)btn.tag];
if (self.block) {
self.block(shopId);
}
}
@end
我将HeaderView的布局写在了这个View里面,还有HeaderView上的按钮的点击事件,通过block回调,在主页面进行页面跳转操作。
总结
可能你会发现这个目录中没有Model,这是因为我做的这个Demo中用Model太浪费,以后,如果我感觉我对MVVM的理解更深一层的时候,会再写一篇关于MVVM的文章,敬请期待啦!
这个Demo中的数据用的是我公司首页的接口,请不要乱用哦!
Demo中用到的网络请求是我再封装的一层,用起来还不错,如果有什么好的建议欢迎提出。
网友评论
这句放到block 中,
否则DetailViewController 不能释放, 也不能赋值
我在VM中放了两个请求,请求回来的数据都调用block,会导致一个VM类两个请求后执行一个block,这个怎么解决啊?
是一个网络请求就要写一个VM类吗?
HeaderVM *headerView = [[HeaderVM alloc] init];
这句话 是不是 初始化的是一个View?
应该写成
@property (copy, nonatomic) ErrorCodeBlock errorBlock;
会更好吧.