折腾了ReactiveCocoa不少天了,前面时间主要停留在看的阶段,今天通过一个小例子来记录一下,我遇到的一些问题,为减少篇幅,CRLoginViewController里面的内容,我主要贴核心部分,其他的UI啦啥的,就不在文章中贴了,例子内容我也简要描述一下,要求用户名为邮箱,要求密码不少于3位,如果不符合规则,则文本框背景为红色,符合规则则为白色,登录按钮摆设用,不是这次描述内容的关注点,所以直接略过吧,直接上图和代码:
原版:
CRLoginViewController.m
1 2 3 4 5 6 7 8 9 10
| RAC(self.userNameTextField, backgroundColor) = [self.userNameTextField.rac_textSignal map:^id(NSString *userName) { NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"; NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex]; return [emailTest evaluateWithObject:userName] ? [UIColor whiteColor] : [UIColor redColor]; }]; RAC(self.passwordTextField, backgroundColor) = [self.passwordTextField.rac_textSignal map:^id(id value) { BOOL result = [value length] > 3; return result ? [UIColor whiteColor] : [UIColor redColor]; }];
|
做完上面这个,我就想着,既然用ReactiveCocoa就是为了优雅,而它也一直推荐着使用MVVM结合使用,那么,这块逻辑判断不应该放在controller,这样不利于复用,也会让controller显得很胖。所以我引入了MVVM中viewModel的概念(不清楚的可以去翻翻谷歌一下),和那些MVP,MVCS啊啥的差不多,目的都是为了瘦controller,而如果你看到那些博客里面在viewModel里面写了UI的东西,那么我可以很负责作地对你说,跳过吧,那文章不值得你看看,viewModel里面拒绝包含UI的东西,否则它将很难复用,扯得有点远了,各位看官不明白的各自科普去吧,下面就讲针对上面版本的另一种实现:
####进化版1:
CRLoginViewModel.h
1 2 3 4 5 6 7 8 9 10 11
| #import <Foundation/Foundation.h> #import <ReactiveCocoa.h> @interface CRLoginViewModel : NSObject @property (nonatomic, copy) NSString *userName; @property (nonatomic, copy) NSString *password; - (BOOL)checkUserNameVaild:(NSString *)userName ; - (BOOL)checkPasswordVaild:(NSString *)password ; @end
|
CRLoginViewModel.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #import "CRLoginViewModel.h" @implementation CRLoginViewModel -(BOOL)checkUserNameVaild:(NSString *)userName { NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"; NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex]; return [emailTest evaluateWithObject:userName]; } - (BOOL)checkPasswordVaild:(NSString *)password { return password.length >3; } @end
|
CRLoginViewController.m的引用变成如下:
1 2 3 4 5 6 7 8 9 10 11 12
| @weakify(self); RAC(self.viewModel, userName) = self.userNameTextField.rac_textSignal; RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal; RAC(self.userNameTextField, backgroundColor) = [self.userNameTextField.rac_textSignal map:^id(NSString *userName) { BOOL isVaild = [self.viewModel checkUserNameVaild:userName]; return isVaild ? [UIColor whiteColor] : [UIColor redColor]; }]; RAC(self.passwordTextField, backgroundColor) = [self.passwordTextField.rac_textSignal map:^id(NSString *password) { BOOL isVaild = [self.viewModel checkPasswordVaild:password]; return isVaild ? [UIColor whiteColor] : [UIColor redColor]; }];
|
####进化版2,这种方式采用了双向监听,即isUserNameVaild监听viewModel里面的userName变化而变化,而viewModel的userName监听文本框的变化而变化
1 2 3 4 5 6 7 8 9 10 11
| #import <Foundation/Foundation.h> #import <ReactiveCocoa.h> @interface CRLoginViewModel : NSObject @property (nonatomic, copy) NSString *userName; @property (nonatomic, copy) NSString *password; @property (nonatomic, strong) NSNumber *isUserNameVaild; @property (nonatomic, strong) NSNumber *isPasswordVaild; - (BOOL)checkUserNameVaild:(NSString *)userName ; - (BOOL)checkPasswordVaild:(NSString *)password ;
|
CRLoginViewModel.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #import "CRLoginViewModel.h" @implementation CRLoginViewModel - (instancetype)init { if (self = [super init]) { @weakify(self); RAC(self, isUserNameVaild) = [RACObserve(self, userName) map:^id(NSString *userName) { @strongify(self); return @([self checkUserNameVaild:userName]); }]; RAC(self, isPasswordVaild) = [RACObserve(self, password) map:^id(NSString *password) { @strongify(self); return @([self checkPasswordVaild:password]); }]; } return self; } -(BOOL)checkUserNameVaild:(NSString *)userName { NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"; NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex]; return [emailTest evaluateWithObject:userName]; } - (BOOL)checkPasswordVaild:(NSString *)password { return password.length >3; } @end
|
CRLoginViewController.m
1 2 3 4 5 6 7 8 9 10 11 12 13
| @weakify(self); RAC(self.viewModel, userName) = self.userNameTextField.rac_textSignal; RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal; RAC(self.userNameTextField, backgroundColor) =[RACObserve(self.viewModel, isUserNameVaild) map:^id(id value) { BOOL isVaild = [value boolValue]; return isVaild ? [UIColor whiteColor] :[UIColor redColor]; }]; RAC(self.passwordTextField, backgroundColor) =[RACObserve(self.viewModel, isPasswordVaild) map:^id(id value) { BOOL isVaild = [value boolValue] ; return isVaild ? [UIColor whiteColor] :[UIColor redColor]; }];
|
####做到这里我发现了个问题,就是RAC原本不就为了少去那些多余的属性变量,像上面这种进化版不正是违背了他的原则吗?所以才有了下面的进化3,用信号来做,具体如下:
CRLoginViewModel.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #import <Foundation/Foundation.h> #import <ReactiveCocoa.h> @interface CRLoginViewModel : NSObject @property (nonatomic, copy) NSString *userName; @property (nonatomic, copy) NSString *password; @property (nonatomic, readonly) RACSignal *userNameSignal; @property (nonatomic, readonly) RACSignal *passwordSignal; - (BOOL)checkUserNameVaild:(NSString *)userName ; - (BOOL)checkPasswordVaild:(NSString *)password ; @end
|
CRLoginViewModel.m
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #import "CRLoginViewModel.h" @interface CRLoginViewModel() @property (nonatomic, strong) RACSignal *userNameSignal; @property (nonatomic, strong) RACSignal *passwordSignal; @end @implementation CRLoginViewModel - (instancetype)init { if (self = [super init]) { @weakify(self); self.userNameSignal = [RACObserve(self, userName) map:^id(NSString *userName) { @strongify(self); return @([self checkUserNameVaild:userName]); }]; self.passwordSignal = [RACObserve(self, password) map:^id(NSString *password) { @strongify(self); return @([self checkPasswordVaild:password]); }]; } return self; } -(BOOL)checkUserNameVaild:(NSString *)userName { NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}"; NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex]; return [emailTest evaluateWithObject:userName]; } - (BOOL)checkPasswordVaild:(NSString *)password { return password.length >3; } @end
|
CRLoginViewController.m
1 2 3 4 5 6 7 8 9 10 11
| @weakify(self); RAC(self.viewModel, userName) = self.userNameTextField.rac_textSignal; RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal; RAC(self.userNameTextField, backgroundColor) = [self.viewModel.userNameSignal map:^id(id value) { return [value boolValue]? [UIColor whiteColor] : [UIColor redColor]; }]; RAC(self.passwordTextField, backgroundColor) = [self.viewModel.passwordSignal map:^id(id value) { return [value boolValue]? [UIColor whiteColor] : [UIColor redColor]; }];
|
代码写完了,接下来就对于上面的几种做法做下个人评价:
- 原版感觉还我们平常编码思想,只是用了RAC来做了部分代码简化,如果你自己定义宏,也能做到一样的效果。
- 进化版1只是简单地做了文件分离,把原本写的controller里面的代码分离出去而已。
- 进化版2看起来像RAC了,ARC的一个特点不就是事件流,避免用临时变量存储某些计算结果么,所以不这符合RAC设计的初衷。
- 进化版3更RAC了,将所有数据包成信号的方式来处理,通常,登录会有需求,验证不通过时,登录按钮应该禁止点击这样的操作,这时,使用信号的好处就更明显了,可以将combineLatest:reduce将两个信号拼接起来,形成一个新的处理信号,好处多多。
至于为什么进化版3有这么多好处,而我还选择拿前面几个例子来说呢,首先一点是为了记录我做这例子时遇到的问题,而这也是很多新手会遇到的问题,而在我学习RAC时,来来回回的博客都是那么几篇,讲的内容都是差不多一样,像原版的做法,就是ray里面的那篇文章的做法,而往往很多教程本身,教人家怎么去使用一个新东西时,只是关注于他里面的语法,举例它是个怎么便捷法,他教你怎么去使用它,而却不告诉你怎么用他才更好,而很多时候新手就在这打住了,以为这就是最佳实践。现在对RAC的认知是,它是为了让controller瘦,它是为了让模块化更清晰,信号机制让编程更清晰,而它和MVVM没有任何关系,只是MVVM能让它变得更好,而不在于它语法的精简。
注:版权声明:本文为博主原创文章,未经博主允许不得转载。