简介
- MVVM:Model–View–Viewmode 是一种软件架构模式。其主作用就是解决Controller代码过于臃肿的问题。因为传统MVC中的Controller要负责View和Model之间调度:网络请求、字典转模型并赋值给view、偶尔还要写一写界面,业务逻辑处理等等,随着APP越来越复杂,导致Controller里的代码越来越臃肿不堪。一不小心Controller里的代码就上到几千行,想象下刚到一家公司就接手这样的项目。。。。。(╯‵□′)╯︵┻━┻
为了解决这个问题,我们可以在MVC的基础上,把C拆出一个ViewModel专门负责数据处理的事情。 - 为什么要使用RAC:为了让View和ViewModel之间能够有比较松散的绑定关系,让代码更加优雅。但是RAC是一个超重量级的框架,学习成本很大,我在之前的文章结合代码示例介绍过一些RAC基本用法,传送🚪:iOS开发之ReactiveCocoa的基本用法干货分享
实战
本文介绍两个开发中常用的场景,第一个是UITableView列表界面通过网络请求数据展示数据,第二个是登录功能。功能比较基础,但都是精髓。分享一下笔者对MVVM的一些见解,在此抛砖引玉,希望能对广大开发者提供一点思路。
一、UITableView列表
效果如上图,实现此功能用到的类:
- Controller --- OrderController
- ViewModel --- RequestViewModel
- View --- OrderCell
- Model --- OrderModel
1、OrderController
#import "OrderController.h"
#import "RequestViewModel.h"
#import "OrderCell.h"
@interface OrderController ()<UITableViewDataSource, UITableViewDelegate>
@property (weak, nonatomic) IBOutlet UITableView *tableView;
@property (strong, nonatomic) RequestViewModel *reqVM;
@end
@implementation OrderController
- (void)viewDidLoad {
[super viewDidLoad];
[self setUI];
[self ViewModelEvent];
}
#pragma mark - 界面设置
- (void)setUI {
self.tableView.dataSource = self;
self.tableView.delegate = self;
self.tableView.rowHeight = 100;
[self.tableView registerNib:[UINib nibWithNibName:@"OrderCell" bundle:nil] forCellReuseIdentifier:@"OrderCell"];
}
#pragma mark - ViewModel事件
- (void)ViewModelEvent {
[self.reqVM.reqCommand execute:nil];
@weakify(self);
[self.reqVM.refreshUISubject subscribeNext:^(id x) {
@strongify(self);
[self.tableView reloadData];
}];
}
#pragma mark - UITableView配置
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.reqVM.dataArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
OrderCell *cell = [tableView dequeueReusableCellWithIdentifier:@"OrderCell"];
cell.model = self.reqVM.dataArray[indexPath.row];
return cell;
}
#pragma mark - 懒加载
- (RequestViewModel *)reqVM {
if (!_reqVM) {
_reqVM = [[RequestViewModel alloc] init];
}
return _reqVM;
}
@end
OrderController主要讲的是ViewModelEvent中的方法,其他也没什么可说的
[self.reqVM.reqCommand execute:nil]; 方法为执行reqCommand事件命令,reqCommand是RequestViewModel中网络请求事件。
[self.reqVM.refreshUISubject subscribeNext:^(id x) {
@strongify(self);
[self.tableView reloadData];
}];
此方法为订阅RequestViewModel中网络请求完成时发送的信号(refreshUISubject),也就是说当网络请求完成之后会执行block中的刷新tableView方法。
2、RequestViewModel:主要向控制器提供数据,通知tableView刷新界面
RequestViewModel.h
#import <Foundation/Foundation.h>
#import <ReactiveObjC/ReactiveObjC.h>
@interface RequestViewModel : NSObject
@property (nonatomic, strong) RACSubject *refreshUISubject;
@property (strong, nonatomic) RACCommand *reqCommand;
@property (nonatomic, strong) NSArray *dataArray;
@end
RequestViewModel.m
#import "RequestViewModel.h"
#import "OrderModel.h"
#import "AFNetworking.h"
#import "MBProgressHUD+Add.h"
#import "MJExtension.h"
@interface RequestViewModel ()
@end
@implementation RequestViewModel
- (instancetype)init {
if (self = [super init]) {
[self or_initialize];
}
return self;
}
- (void)or_initialize {
[self.reqCommand.executionSignals.switchToLatest subscribeNext:^(NSDictionary *dic) {
NSArray *items = dic[@"items"];
self.dataArray = [OrderModel mj_objectArrayWithKeyValuesArray:items];
[self.refreshUISubject sendNext:nil];
}];
[[self.reqCommand.executing skip:1] subscribeNext:^(id x) {
if ([x isEqualToNumber:@(YES)]) {
[MBProgressHUD showCircleHud:nil];
}else {
[MBProgressHUD closeHud:nil];
}
}];
}
#pragma mark - 懒加载
- (RACCommand *)reqCommand {
if (!_reqCommand) {
_reqCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
//因为要把请求的数据传出去,所以要把网络请求包装在信号里
RACSignal *signal = [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
NSDictionary *dic = @{@"action":@"getProduct",@"page":@"0"};
NSString *url = @"http://10.49.3.125:8080/";
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.requestSerializer = [AFHTTPRequestSerializer serializer];
[manager GET:url parameters:dic progress:^(NSProgress * _Nonnull downloadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSError * error;
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:&error];
[subscriber sendNext:dic];
[subscriber sendCompleted];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[MBProgressHUD showMessage:@"网络连接失败" toView:nil];
[subscriber sendCompleted];
}];
return nil;
}];
//返回网络请求信号
return signal;
}];
}
return _reqCommand;
}
- (RACSubject *)refreshUISubject {
if (!_refreshUISubject) {
_refreshUISubject = [RACSubject subject];
}
return _refreshUISubject;
}
- (NSArray *)dataArray {
if (!_dataArray) {
_dataArray = [[NSArray alloc] init];
}
return _dataArray;
}
@end
- RequestViewModel.h
refreshUISubject属性是通知控制器刷新UI的信号,其功能类似于代理。reqCommand属性是网络请求事件,暴露在 .h 文件的原因是让控制器来决定什么时候发起事件,也就是说什么时候发起网络请求。 - RequestViewModel.m
or_initialize 中第一个方法是订阅reqCommand(网络请求)事件中的信号发出的值,也就是网络请求成功后发送的数据。第二个方法的功能是监听reqCommand事件过程,其block中的值返回YES是,代表事件正在执行,所以在这里面可以加一个正在加载的菊花,当返回值为NO时,代表事件执行完成,把正在加载菊花去掉。 - 懒加载 - (RACCommand *)reqCommand 方法中就是网络请求事件,block里面的signal信号作用是把网络请求的数据发送给 or_initialize 中第一个方法的订阅者。订阅者拿到数据后执行字典转模型操作,然后发送暴露在.h文件中的 refreshUISubject 信号给订阅此信号的控制器,通知他刷新tableView。
3、OrderCell和OrderModel
跟之前MVC做法完全一致,其实没什么好说的
OrderCell.h
#import <UIKit/UIKit.h>
#import "OrderModel.h"
@interface OrderCell : UITableViewCell
@property (nonatomic, strong) OrderModel *model;
@end
OrderCell.m
#import "OrderCell.h"
#import "SDWebImage.h"
@interface OrderCell ()
@property (weak, nonatomic) IBOutlet UIImageView *imgV;
@property (weak, nonatomic) IBOutlet UILabel *nameLab;
@property (weak, nonatomic) IBOutlet UILabel *typeLab;
@property (weak, nonatomic) IBOutlet UILabel *descLab;
@end
@implementation OrderCell
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
}
- (void)setModel:(OrderModel *)model {
[_imgV sd_setImageWithURL:[NSURL URLWithString:model.imageUrl]];
_nameLab.text = model.name;
_typeLab.text = model.type;
_descLab.text = model.desc;
}
@end
OrderModel.h
#import <Foundation/Foundation.h>
@interface OrderModel : NSObject
@property (nonatomic, copy) NSString *desc;
@property (nonatomic, copy) NSString *imageUrl;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *type;
@end
二、登录功能
效果如上图,实现此功能用到的类:
- Controller --- LoginController
- ViewModel --- LoginViewModel
1、LoginController
#import "LoginController.h"
#import "LoginViewModel.h"
@interface LoginController ()
@property (weak, nonatomic) IBOutlet UITextField *numTextField;
@property (weak, nonatomic) IBOutlet UITextField *pwdTextField;
@property (weak, nonatomic) IBOutlet UIButton *loginBtn;
@property (strong, nonatomic) LoginViewModel *loginVM;
@end
@implementation LoginController
- (void)viewDidLoad {
[super viewDidLoad];
[self bindViewModel];
[self loginEvent];
}
#pragma mark - ViewModel处理
- (void)bindViewModel {
//给ViewModel账号密码绑定信号
RAC(self.loginVM,num) = _numTextField.rac_textSignal;
RAC(self.loginVM,pwd) = _pwdTextField.rac_textSignal;
}
- (void)loginEvent {
//把_loginBtn的enabled属性与信号绑定
RAC(_loginBtn,enabled) = self.loginVM.loginEnabledSignal;
//登录按钮点击事件
[[_loginBtn rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(__kindof UIControl * _Nullable x) {
[self.loginVM.loginCommand execute:nil ];
}];
}
#pragma mark - 懒加载
- (LoginViewModel *)loginVM {
if (!_loginVM) {
_loginVM = [[LoginViewModel alloc] init];
}
return _loginVM;
}
@end
bindViewModel方法
用的RAC()宏,将LoginViewModel对象的num(账号)和pwd(密码)属性分别和_numTextField、_pwdTextField输入的文字绑定。简单的说,就是将控制器界面中的账号和密码输入框的内容传给LoginViewModel,并且输入框里的内容每次改变都要重新传过去。因为我们要在ViewModel中处理业务逻辑,所以要把值传给它。-
loginEvent方法
第一个方法同样是用RAC()宏,将登录按钮是否可点击属性绑定LoginViewModel的loginEnabledSignal信号,以达到在 LoginViewModel 中写控制按钮是否能点击的逻辑。第二个方法是监听按钮点击事件, 当按钮点击时,执行loginCommand(登录事件)命令。
2、LoginViewModel
- LoginViewModel.h
#import <Foundation/Foundation.h>
#import <ReactiveObjC/ReactiveObjC.h>
@interface LoginViewModel : NSObject
@property (copy, nonatomic) NSString *num;
@property (copy, nonatomic) NSString *pwd;
//按钮是否被允许点击
@property (strong, nonatomic, readonly) RACSignal *loginEnabledSignal;
//登录按钮命令
@property (strong, nonatomic, readonly) RACCommand *loginCommand;
@end
- LoginViewModel.m
#import "LoginViewModel.h"
#import "MBProgressHUD+Add.h"
@implementation LoginViewModel
- (instancetype)init {
if (self = [super init]) {
[self setRACSignal];
}
return self;
}
- (void)setRACSignal {
_loginEnabledSignal = [RACSignal combineLatest: @[RACObserve(self, num),RACObserve(self, pwd)] reduce:^id (NSString *num, NSString *pwd){
//账号输入位数大于0,密码大于等于6时登录按钮可点击
BOOL isEnabled = (num.length > 0 && pwd.length >= 6) ? YES : NO;
return @(isEnabled);
}];
//处理登录点击:创建登录命令。(只要处理事件,就要用到RACCommand)
_loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [RACSignal createSignal:^RACDisposable * _Nullable(id<RACSubscriber> _Nonnull subscriber) {
//模拟请求登录数据
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[subscriber sendNext:@"模拟登录请求"];
[subscriber sendCompleted]; //一定要写
});
return nil;
}];
}];
//订阅命令中的信号
[_loginCommand.executionSignals.switchToLatest subscribeNext:^(id _Nullable x) {
//这里写保存服务器返回的信息
}];
//监听命令执行过程
//skip:1跳过第一次信号,因为刚开始没有执行的时候x也为NO
[[_loginCommand.executing skip:1] subscribeNext:^(NSNumber * _Nullable x) {
if ([x boolValue] == YES) {
[MBProgressHUD showCircleHud:nil];
}else {
//执行完成
[MBProgressHUD closeHud:nil];
[MBProgressHUD showMessage:@"登陆成功" toView:nil];
}
}];
}
@end
- setRACSignal方法
1.第一个方法创建按钮是否点击的信号赋值给控制器,其中 combineLatest 方法是将数组中的信合组成为一个新的信号。其中任何一个信号发送数据,组成的信号都能执行订阅后的代码块。RACObserve()用法类似于KVO,只要监听的属性改变就会发送信号。
2.第二个方法为登录处理事件逻辑,block里的RACSignal信号中可以写登录的网络请求,
3.第三个方法为订阅登录网络请求产生的数据,在其block中可以写一些处理网络返回数据的逻辑,例如:保存用户信息
4.第四个方法为监听执行登录命令的过程与例1UITableView网络请求中的用法一致。