ReactiveCocoa 实现登录逻辑

  折腾了ReactiveCocoa不少天了,前面时间主要停留在看的阶段,今天通过一个小例子来记录一下,我遇到的一些问题,为减少篇幅,CRLoginViewController里面的内容,我主要贴核心部分,其他的UI啦啥的,就不在文章中贴了,例子内容我也简要描述一下,要求用户名为邮箱,要求密码不少于3位,如果不符合规则,则文本框背景为红色,符合规则则为白色,登录按钮摆设用,不是这次描述内容的关注点,所以直接略过吧,直接上图和代码:

ReactiveCocoa Login

原版:

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能让它变得更好,而不在于它语法的精简。


注:版权声明:本文为博主原创文章,未经博主允许不得转载。