MVVM的結(jié)構(gòu)
我們將MVC
的Controller
中的邏輯模塊抽取到ViewModel
中,使Controller
只負(fù)責(zé)界面顯示,這里Controller
通過綁定與ViewModel
進(jìn)行數(shù)據(jù)通知.即:
model
層望门,API請(qǐng)求的原始數(shù)據(jù)
view
層,視圖展示呆万,由viewController來控制
viewModel
層蚓挤,負(fù)責(zé)業(yè)務(wù)處理和數(shù)據(jù)轉(zhuǎn)化
MVVM的特點(diǎn)
- MVVM方便測(cè)試
- 方便業(yè)務(wù)的復(fù)用
- 方便對(duì)職責(zé)進(jìn)行劃分,例如可以讓初級(jí)開發(fā)開發(fā)UI,高級(jí)開發(fā)開發(fā)邏輯
- 代碼更加優(yōu)雅,增加可維護(hù)性
ReactiveCocoa
本文使用ReactiveCocoa
來完成了綁定這個(gè)功能,但使用它之前首先需要了解他的幾個(gè)特點(diǎn):
- 學(xué)習(xí)成本很高
- 調(diào)試需要較高的技巧
- 這是一個(gè)很重型的框架,幾乎替代了蘋果官方所有的事件機(jī)制
- 每個(gè)人的代碼風(fēng)格都不盡相同,使用該框架需要團(tuán)隊(duì)成員幾乎每天都要互相
review
團(tuán)隊(duì)成員的代碼 - 好處當(dāng)然也是有的:響應(yīng)式,函數(shù)式,高聚合,低耦合
代碼分析
需求: 當(dāng)用戶輸入完手機(jī)號(hào)與四位驗(yàn)證碼時(shí),進(jìn)行手機(jī)號(hào)規(guī)則校驗(yàn),通過后進(jìn)行登錄請(qǐng)求
Controller層
我們可以看到,Controller
層僅僅是單向綁定了輸入框與ViewModel
的字符屬性,代碼已經(jīng)很簡(jiǎn)潔了.
#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{
//電話號(hào)碼
RAC(self.viewModel,mobileNum) = self.mobileNumTF.rac_textSignal;
//驗(yàn)證碼
RAC(self.viewModel,smsCode) = self.smsCodeTF.rac_textSignal;
}
@end
ViewModel層
- 1頭文件
頭文件這里command在外界并沒與用上,但其他viewModel可能會(huì)用上所以還是拿了出來.
#import "BaseViewModel.h"
@interface MRCLoginViewModel : BaseViewModel
@property (nonatomic,copy)NSString *mobileNum;
@property (nonatomic,copy)NSString *smsCode;
@property (nonatomic,strong,readonly) RACCommand *loginCommand;
@end
- 2實(shí)現(xiàn)文件
這里有幾個(gè)坑需要說一下:
- RAC的KVO寫法不能觀察系統(tǒng)的只讀屬性(其實(shí)OC也不能)
- 觀察對(duì)象必須賦初值,否則可能有很奇怪的問題
#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]) {
//這里字符串必須初始化!,否則數(shù)組會(huì)崩潰
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(@"觸發(fā)請(qǐng)求");
[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);
//這里位數(shù)以及驗(yàn)證碼在控制器那里已經(jīng)篩選過了,這里只是做個(gè)簡(jiǎn)單的例子,可以在這里對(duì)手機(jī)正則等進(jìn)行校驗(yàn)
//最好的寫法:button.rac_command = viewmodel.loginCommand...把位數(shù)判斷移到這里
if (self.mobileNum.length != 11) {
return [RACSignal error:[NSError errorWithDomain:@"" code:10 userInfo:@{@"errorInfo":@"手機(jī)號(hào)碼位數(shù)不對(duì)"}]];
}
if (self.smsCode.length != 4) {
return [RACSignal error:[NSError errorWithDomain:@"" code:20 userInfo:@{@"errorInfo":@"驗(yàn)證碼位數(shù)不對(duì)"}]];
}
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參數(shù)
NSMutableDictionary *params = [[NSMutableDictionary alloc]init];
[params setObject:[self deviceNo] forKey:@"deviceNo"];
NSDictionary * dict = @{@"mobileNo":mobileNo,
@"authCodeSMS":authCodeSMS};
NSMutableDictionary *newParams = [dict mutableCopy];
[newParams addEntriesFromDictionary:params];
//網(wǎng)絡(luò)請(qǐng)求
[FYRequestTool POST:@"" parameters:newParams progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//發(fā)送成功內(nèi)容(需要什么發(fā)什么,也可以直接給單例賦值)
[subscriber sendNext:responseObject];
[subscriber sendCompleted];
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
[subscriber sendError:error];
}];
//完成信號(hào)后取消
return [RACDisposable disposableWithBlock:^{
[FYRequestTool cancel];
}];
}];
}
- (NSString *)deviceNo
{
return [SimulateIDFA createSimulateIDFA];
}
@end