MVVM的结构
我们将MVC
的Controller
中的逻辑模块抽取到ViewModel
中,使Controller
只负责界面显示,这里Controller
通过绑定与ViewModel
进行数据通知.即:
model
层,API请求的原始数据
view
层,视图展示,由viewController来控制
viewModel
层,负责业务处理和数据转化
MVVM的特点
- MVVM方便测试
- 方便业务的复用
- 方便对职责进行划分,例如可以让初级开发开发UI,高级开发开发逻辑
- 代码更加优雅,增加可维护性
ReactiveCocoa
本文使用ReactiveCocoa
来完成了绑定这个功能,但使用它之前首先需要了解他的几个特点:
- 学习成本很高
- 调试需要较高的技巧
- 这是一个很重型的框架,几乎替代了苹果官方所有的事件机制
- 每个人的代码风格都不尽相同,使用该框架需要团队成员几乎每天都要互相
review
团队成员的代码 - 好处当然也是有的:响应式,函数式,高聚合,低耦合
代码分析
需求: 当用户输入完手机号与四位验证码时,进行手机号规则校验,通过后进行登录请求
Controller层
我们可以看到,Controller
层仅仅是单向绑定了输入框与ViewModel
的字符属性,代码已经很简洁了.
#import "MRCLoginController.h"
#import "MRCLoginViewModel.h"
@interface MRCLoginController ()
@property (weak, nonatomic) IBOutlet UITextField *mobileNumTF;
@property (weak, nonatomic) IBOutlet UITextField *smsCodeTF;
@property (strong, nonatomic) MRCLoginViewModel *viewModel;
@end
@implementation MRCLoginController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view from its nib.
self.viewModel = [[MRCLoginViewModel alloc] init];
//绑定
[self p_bindViewModel];
}
#pragma mark - bind
- (void)p_bindViewModel{
//电话号码
RAC(self.viewModel,mobileNum) = self.mobileNumTF.rac_textSignal;
//验证码
RAC(self.viewModel,smsCode) = self.smsCodeTF.rac_textSignal;
}
@end
ViewModel层
- 1头文件
头文件这里command在外界并没与用上,但其他viewModel可能会用上所以还是拿了出来.
#import "BaseViewModel.h"
@interface MRCLoginViewModel : BaseViewModel
@property (nonatomic,copy)NSString *mobileNum;
@property (nonatomic,copy)NSString *smsCode;
@property (nonatomic,strong,readonly) RACCommand *loginCommand;
@end
- 2实现文件
这里有几个坑需要说一下:
- RAC的KVO写法不能观察系统的只读属性(其实OC也不能)
- 观察对象必须赋初值,否则可能有很奇怪的问题
#import "MRCLoginViewModel.h"
#import "FYRequestTool.h"
#import "SimulateIDFA.h"
@interface MRCLoginViewModel ()
@property (nonatomic,strong,readwrite) RACCommand *loginCommand;
@end
@implementation MRCLoginViewModel
- (instancetype)init{
if (self = [super init]) {
//这里字符串必须初始化!,否则数组会崩溃
self.mobileNum = @"";
self.smsCode = @"";
RACSignal * mobileNumSignal = [RACObserve(self, mobileNum) filter:^BOOL(NSString * value) {
return value.length == 11;
}];
RACSignal * smsCodeSignal =[RACObserve(self, smsCode) filter:^BOOL(NSString * value) {
return value.length == 4;
}];
@weakify(self);
[[RACSignal combineLatest:@[mobileNumSignal,smsCodeSignal]] subscribeNext:^(id x) {
@strongify(self);
NSLog(@"触发请求");
[self.loginCommand execute:nil];
}];
}
return self;
}
#pragma mark - get && set
- (RACCommand *)loginCommand{
if (nil == _loginCommand) {
@weakify(self);
_loginCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {
@strongify(self);
//这里位数以及验证码在控制器那里已经筛选过了,这里只是做个简单的例子,可以在这里对手机正则等进行校验
//最好的写法:button.rac_command = viewmodel.loginCommand...把位数判断移到这里
if (self.mobileNum.length != 11) {
return [RACSignal error:[NSError errorWithDomain:@"" code:10 userInfo:@{@"errorInfo":@"手机号码位数不对"}]];
}
if (self.smsCode.length != 4) {
return [RACSignal error:[NSError errorWithDomain:@"" code:20 userInfo:@{@"errorInfo":@"验证码位数不对"}]];
}
return [self loginSignalWithMobileNum:self.mobileNum smsCode:self.smsCode];
}];
}
return _loginCommand;
}
#pragma mark - private
- (RACSignal *)loginSignalWithMobileNum:(NSString *)mobileNo smsCode:(NSString *)authCodeSMS{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
//添加deviceID参数
NSMutableDictionary *params = [[NSMutableDictionary alloc]init];
[params setObject:[self deviceNo] forKey:@"deviceNo"];
NSDictionary * dict = @{@"mobileNo":mobileNo,
@"authCodeSMS":authCodeSMS};
NSMutableDictionary *newParams = [dict mutableCopy];
[newParams addEntriesFromDictionary:params];
//网络请求
[FYRequestTool POST:@"" parameters:newParams progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//发送成功内容(需要什么发什么,也可以直接给单例赋值)
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[subscriber sendError:error];
}];
//完成信号后取消
return [RACDisposable disposableWithBlock:^{
[FYRequestTool cancel];
}];
}];
}
- (NSString *)deviceNo
{
return [SimulateIDFA createSimulateIDFA];
}
@end