基础:
RACCommand的详细讲解
MVVM架构已经耳熟能详,网上有非常多的文章,讲的都相当不错,MVVM最主要的关系我这里要提醒一下
最基本的规则:
1.View持有ViewModel 反之不持有
2.ViewModel持有Model 反之不持有
网上copy了一张关系图
MVVM 的基本概念
在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件
view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)
viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方
使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
MVVM 的注意事项
view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
viewModel 引用model,但反过来不行
MVVM 的使用建议
MVVM 可以兼容你当下使用的MVC架构。
MVVM 增加你的应用的可测试性。
MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。
viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。
viewModel之间可以有依赖。
viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
MVVM 的优势
低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上
可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
MVVM 的弊端
数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
(以上是网上copy来的)
pod
target 'demo' do
pod 'AFNetworking', '~> 3.0'
pod 'FBSDKCoreKit', '~> 4.9'
pod 'ReactiveObjC', '~> 3.1.0'
pod 'SVProgressHUD', '~> 2.2.5'
end
LoginModel
#import
@interface LoginModel : NSObject
@property (strong, nonatomic) NSString *userId;
@property (strong, nonatomic) NSString *displayName;
@end
LoginViewModel
持有Model
网络请求,数据验证等等可以放在这里面处理 使用RAC进行数据的双向绑定 RAC是以信号的形式进行数据通信=》解耦 如果没有RAC的绑定机制,那我们需要自己使用delegate KVO等来实现数据的回调相对头疼
V 和 VM之间通过RACCommand命令做事件与数据的绑定 RACCommand不关心业务,它只负责业务间的通信(RACCommand会涉及RACSignal需要先去了解一下) V跟VM通过这个命令去做对应的逻辑处理,处理完毕后VM会通过信号[subscriber sendNext:xxx] 将数据回调给V,V只要订阅这个cmd的信号就可以收到数据了, 或者可以用V配合RACObserve来观察数据结果做展示也是可以的(KVO形式)
#import
#import
#import "LoginModel.h"
@interface LoginViewModel : NSObject
@property (nonatomic,copy) NSString *account;
@property (nonatomic,copy) NSString *pwd;
@property (nonatomic,strong) NSError *error;
@property (nonatomic,strong) LoginModel *loginModel;
@property (nonatomic,strong) RACCommand *loginBtnEnableCmd;
@property (nonatomic,strong) RACCommand *loginActionCmd;
@end
#import "LoginViewModel.h"
#import
@implementation LoginViewModel
- (instancetype)init{
if (self = [super init]) {
//双向绑定
[self initCommand];
[self initSubscribe];
}
return self;
}
- (void)initSubscribe {
[RACObserve(self, account) subscribeNext:^(id _Nullable x) {
[self checkSubmitEnable];
}];
[RACObserve(self, pwd) subscribeNext:^(id _Nullable x) {
[self checkSubmitEnable];
}];
}
- (void)initCommand {
self.loginBtnEnableCmd = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
return [self racForSubmitEnable];
}];
self.loginActionCmd = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(id _Nullable input) {
return [self racForlogin];
}];
}
- (void)checkSubmitEnable {
[self.loginBtnEnableCmd execute:nil];
}
//校验登录按钮的状态
- (RACSignal *)racForSubmitEnable {
return [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) {
//模拟:3个字符便可
BOOL status = self.account.length == 3 && self.pwd.length == 3;
[subscriber sendNext:@(status)];
[subscriber sendCompleted];
return nil;
}];
}
//登录操作
- (RACSignal *)racForlogin {
return [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) {
//模拟网络请求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
LoginModel *result = [LoginModel new];
if ([self.account isEqualToString:@"123"] && [self.pwd isEqualToString:@"123"]) {
//模拟请求结果
result.userId = [NSString stringWithFormat:@"%@%@",self.account,self.pwd];
result.displayName = result.userId;
self.error = nil;
} else {
//模拟错误
self.error = [NSError errorWithDomain:@"-1" code:-1 userInfo:@{@"des":@"账号密码错误"}];
}
self.loginModel = result;
//结果发送给监听方
[subscriber sendNext:self];
//设置完成
[subscriber sendCompleted];
});
return nil;
}];
}
@end
ViewController
持有ViewModel 所有的逻辑都在ViewModel里面处理而自己只做简单的数据展示到控件上 不直接跟Model打交道
RACCommand进行数据的双向绑定通信
#import "ViewController.h"
#import
#import "RAC.h"
#import
#import "LoginViewModel.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UITextField *txtAccount;
@property (weak, nonatomic) IBOutlet UITextField *txtPwd;
@property (weak, nonatomic) IBOutlet UIButton *btnSubmit;
@property (weak, nonatomic) IBOutlet UILabel *lblResult;
@property (strong,nonatomic) LoginViewModel *viewModel;
- (IBAction)touchAction:(id)sender;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupUI];
//双向绑定
[self initCommand];
[self initSubscribe];
//kvo监听数据回调
//[self bindViewModel];
}
- (void)setupUI {
self.btnSubmit.enabled = NO;
[self.btnSubmit setBackgroundImage:[self createImageWithColor:[UIColor blueColor]] forState:UIControlStateNormal];
[self.btnSubmit setBackgroundImage:[self createImageWithColor:[UIColor grayColor]] forState:UIControlStateDisabled];
}
-(UIImage*)createImageWithColor:(UIColor*) color{
CGRect rect=CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
UIGraphicsBeginImageContext(rect.size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, [color CGColor]);
CGContextFillRect(context, rect);
UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return theImage;
}
- (void)initCommand {
//关联赋值
RAC(self.viewModel,account) = self.txtAccount.rac_textSignal;
RAC(self.viewModel,pwd) = self.txtPwd.rac_textSignal;
}
- (void)initSubscribe {
//VM内部处理按钮的可用状态 直接将结果告诉V
[[self.viewModel.loginBtnEnableCmd.executionSignals switchToLatest] subscribeNext:^(id _Nullable x) {
self.btnSubmit.enabled = [x boolValue];
self.lblResult.text = @"";
}];
//监听当前命令是否正在执行executing
//监听命令是否执行完毕,默认会来一次,可以直接跳过,skip表示跳过第一次信号。
//x:YES 当前cmd正在触发执行
//x:NO 当前cmd不处于执行状态/或已处理完成
[[self.viewModel.loginActionCmd.executing skip:1] subscribeNext:^(id _Nullable x) {
if ([x boolValue]) {
[SVProgressHUD showWithStatus:@"正在请求"];
}else{
//[SVProgressHUD showSuccessWithStatus:@"加载完毕"];
}
}];
//监听数据回调
[[self.viewModel.loginActionCmd.executionSignals switchToLatest] subscribeNext:^(id _Nullable x) {
LoginViewModel *loginViewModel = x;
if (!loginViewModel) {
return ;
}
if (loginViewModel.error) {
//错误信息自行处理
self.lblResult.text = [NSString stringWithFormat:@"%@",loginViewModel.error.userInfo[@"des"]];
[SVProgressHUD showInfoWithStatus:@"登录失败"];
} else {
self.lblResult.text = loginViewModel.loginModel.userId;
[SVProgressHUD showSuccessWithStatus:@"登录成功"];
}
}];
}
/*
- (void)bindViewModel {
//监听登录结果
[RACObserve(self.viewModel, loginModel) subscribeNext:^(id _Nullable x) {
LoginModel *loginModel = x;//监听的是LoginModel对象的改变
if (!loginModel) {
return ;
}
if (self.viewModel.error) {
//错误信息自行处理
self.lblResult.text = [NSString stringWithFormat:@"%@",self.viewModel.error.userInfo[@"des"]];
[SVProgressHUD showInfoWithStatus:@"登录失败"];
} else {
self.lblResult.text = loginModel.userId;
[SVProgressHUD showSuccessWithStatus:@"登录成功"];
}
}];
}
*/
- (IBAction)touchAction:(id)sender {
//以cmd的形式进行通信 告诉VM发起login请求(结果上面已经有做监听)
[self.viewModel.loginActionCmd execute:nil];
}
-(LoginViewModel *)viewModel{
if (_viewModel == nil) {
_viewModel = [LoginViewModel new];
}
return _viewModel;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
结果 gif