Reactive Cocoa 之旅
我是前言
这是我在简书上的第一篇文章,目的是为了记录一些学习过的知识,以供日后复习。当然,如果本人的一些文章能够帮助到一些人更快、更好的完成工作或者知识扩充,本人感到非常荣幸。之前也想过写一些东西,不过因为种种原因,未能执行,本人会抽出时间回顾和修正文章中的各种问题,如果有什么疑问或者纰漏,欢迎指正。
关于本文
本文主要是学习raywenderlich的Reactive Cocoa tutorial系列教程,并加以个人理解。作者Colin Eberhardt有不少好的教程值得大家学习,话说raywenderlich真心是一个不错的网站,希望没有听说过或者听说过没有学习过的小伙伴们进去转一转,说不定会有所收获。废话不多所,进入主题。
Whats Reactive Cocoa?
Reactive Cocoa
(也叫做RAC
,下文统称RAC)是一个支持FRP
(函数响应式编程)的框架,其灵感来自这篇博客。作为iOSer,我们写的每一行代码都是为了得到一个输出,比如说一个按钮点击之后,处理相应的事件;一个UISearchBar的文本改变,提示不同的信息;用户下拉刷新,获取网络数据以展示...所有的事件都是为了得到一个输出(结果)... 简单输入输出,可能像“Hello world”一样,而复杂的输出,可能要做一系列的操作之后,得到一系列的结果。
Q:在Cocoa的世界中,为了得到这个(些)结果,我们都采取了什么方式呢?
A:Target Action;Blocks;Delegations;Notifications;KVO;Threads and so on..
如果设计模式没用好,架构没弄好,那么我们的代码常常看起来就像是面条一样,难懂又难看。在RAC的世界中,以事件流(用Signal、SignalProductor来表示)的形式,组合和转换信号,最终得到我们想要的输出,代码大统一。
Usage
目前的RAC支持OC和Swift两个版本,前一段时间写了一段swift,不过现在的swift像外挂一样,打出来的IPA包比OC的大很多很多,所以在这里,暂时不提swift版本,日后有机会回来补上。
Round 1 UIControl Target Actions
需求来了:那个xxx啊,我们开个会,关于这个注册功能的。balabalabala...大概设计成这样
简陋的原型界面比较简单,一个常规的注册页面。要求:用户名长度必须大于3,密码长度大于5,否则textField背景色为黄色,正确则为白色;密码框内的内容要和确认密码框的内容一致,否则确认密码框的背景色为黄色;若上述条件都满足,注册按钮可以点击,否则不可点击。
规则简单,也很合理,那么我们需要怎么做呢?
这个简单,看我的:
方案1(With out RAC):
Storyboard中大概这样:
Storyboard
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, weak, nullable) IBOutlet UITextField *userNameField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *passwordField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *confirmField;
@property (nonatomic, weak, nullable) IBOutlet UIButton *signUpButton;
@end
@implementation ViewController
- (IBAction)didChangedUserNameFieldEditing:(id)sender {
self.userNameField.backgroundColor = [self isValidUserName] ? [UIColor whiteColor] : [UIColor yellowColor];
self.signUpButton.enabled = [self shouldSignUp];
}
- (IBAction)didChangedPasswordFieldEditing:(id)sender {
self.passwordField.backgroundColor = [self isValidPassword] ? [UIColor whiteColor] : [UIColor yellowColor];
self.confirmField.backgroundColor = [self isValidConfirm] ? [UIColor whiteColor] : [UIColor yellowColor];
self.signUpButton.enabled = [self shouldSignUp];
}
- (IBAction)didChangedConfirmFieldEditing:(id)sender {
self.confirmField.backgroundColor = [self isValidConfirm] ? [UIColor whiteColor] : [UIColor yellowColor];
self.signUpButton.enabled = [self shouldSignUp];
}
- (BOOL)isValidUserName {
return self.userNameField.text.length > 3;
}
- (BOOL)isValidPassword {
return self.passwordField.text.length > 5;
}
- (BOOL)isValidConfirm {
return [self isValidPassword] && [self.confirmField.text isEqualToString:self.passwordField.text];
}
- (BOOL)shouldSignUp {
return [self isValidUserName] && [self isValidPassword] && [self isValidConfirm];
}
@end
思路很简单:
- userNameField逻辑最简单,根据用户名的长度,修改其输入框背景颜色,判断注册按钮的enable状态,对其更新;
- 用户输入的密码如果符合规则,修改passwordField背景色。同时,如果confirmField符合规则,更新confirmField的背景色,判断注册按钮的enable状态,对其更新;
- 判断confirmField的文字,修改其背景色,判断注册按钮的enable状态,对其更新;
- 点击signUpButton,获得龙虾一只。
可以发现,在每一个TextField文本改变的时候,我们都要去判断并更新signUpButton的enable状态,并且在编辑passwordField文本的时候,判断并更新confirmField的背景色;
来看看如果使用RAC怎么搞呢?
方案2 (With in RAC) :
#import "ViewController.h"
#import <ReactiveCocoa/ReactiveCocoa.h>
@interface ViewController ()
@property (nonatomic, weak, nullable) IBOutlet UITextField *userNameField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *passwordField;
@property (nonatomic, weak, nullable) IBOutlet UITextField *confirmField;
@property (nonatomic, weak, nullable) IBOutlet UIButton *signUpButton;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
RACSignal *userNameSignal = [self.userNameField.rac_textSignal map:^id(NSString *text) {
return @(isValidInput(text, 3));
}];
RACSignal *passwordSignal = [self.passwordField.rac_textSignal map:^id(NSString *text) {
return @(isValidInput(text, 5));
}];
RACSignal *confirmSignal = [RACSignal combineLatest:@[passwordSignal, self.confirmField.rac_textSignal] reduce:^id(NSNumber *passwordValid, NSString *text) {
return @([text isEqualToString:self.passwordField.text] && passwordValid.boolValue);
}];
[userNameSignal subscribeNext:^(NSNumber *valid) {
self.userNameField.backgroundColor = colorWithFlag(valid.boolValue);
}];
[passwordSignal subscribeNext:^(NSNumber *valid) {
self.passwordField.backgroundColor = colorWithFlag(valid.boolValue);
}];
[confirmSignal subscribeNext:^(NSNumber *flag) {
self.confirmField.backgroundColor = colorWithFlag(flag.boolValue);
}];
[[RACSignal
combineLatest:@[userNameSignal, passwordSignal, confirmSignal] reduce:^id(NSNumber *userNameValid, NSNumber *passwordValid, NSNumber *confirmValid) {
return @(userNameValid.boolValue && passwordValid.boolValue && confirmValid.boolValue);
}] subscribeNext:^(NSNumber *allValid) {
self.signUpButton.enabled = allValid.boolValue;
}];
[self.signUpButton rac_signalForControlEvents:UIControlEventTouchUpInside];
}
BOOL isValidInput(NSString *input, NSUInteger givenLength) {
return input.length > givenLength;
}
UIColor * colorWithFlag(BOOL flag) {
return flag ? [UIColor whiteColor] : [UIColor yellowColor];
}
@end
可以看到,用了RAC之后,我们不需要再去写action,或者IBOutlet,通篇都是一些Signal之类的东西。我大概的思路是这样的:
- 观察userNameField的
rac_textSignal
(RAC对许多类都有其不同的Signal,如果感兴趣可以去看一看),textSignal、textSignal,顾名思义,一个文本事件流,发出的信号是一个NSString *类型的对象,然后通过map:
方法将信号转换成一个BOOL含义的NSNumber标识(我们发出、转换或者合并的信号量都是NSObject类型,如果需要返回基本类型,例如此处的BOOL类型,需要将基本类型升级成对象,例如此处的NSNumber *),然后通过subscirbeNext:
方法订阅(个人理解为接收一个事件流,事件流发出的信号,在订阅期间可以接收到)这个信号,如果接收到的信号为真,代表合法的用户名,改变userNameField背景色为白色,否则背景色为黄色。passwordField同理; - confirmField只有在密码合法并且其文本与passwordField的文本相同时,才改变其颜色为白色,否则为黄色,用RAC该怎么处理呢?在这里,我选择了combine(组合)的方式。
- 将confirmField.rac_textSignal与passwordSignal通过
combineLatest:
方法组合成一个新的事件流confirmSignal,也就是说,用户每次编辑passwordField或者confirmField的时候,我们都能接收到confirmSignal发出BOOL含义的NSNumber *信号,然后订阅这个事件流,通过信号来更新其背景色;
- 那么更新signUpButton的逻辑同上,将三个事件流组合成一个新的事件流,订阅事件流,根据信号来更新enable状态;
- 事件流的处理到此为止,接下来,我们要点击注册按钮的时候,push一个页面,得到我们想要的龙虾,既然不使用target action的方式,那么RAC给我们提供了一个 [UIButton rac_signalForControlEvents:]方法来注册一个事件流,这里我们并没有对其处理,所以写成
[self.signUpButton rac_signalForControlEvents:UIControlEventTouchUpInside];
#######个人理解:我认为,RAC这样写的好处,在订阅每一个事件流的时候,只处理这一个事件,不做多余的操作和判断;当多个事件决定同一个结果时,可以将事件流组合,而不会将逻辑拆分到各个action、delegate..中;统一我们的各种事件处理;更好的支持函数式编程;提高了程序的可读性和可维护性,更多的好处期待大家一起来发掘。
尾声咯
差不多逻辑就是这样了,但是我们并没有处理block的retain-cycle,RAC提供了两个很有意思的macro: @weakify(), @strongify()
,会大大帮助我们减少代码量,相信如果没有这两个宏,RAC写起来也会很难看,可以自行Google。
还有一件事,每次写这句话,就好像<成龙历险记>中的老爹...
我们的RAC推荐这样的写法
self.userNameField.rac_textSignal
__filter: ...(这个是干嘛的,自己看看呗,这个玩意儿可以用来简化我们的代码的)
__map: ....
__reduce: ...
__subscribeNext:... (PS: 前面的双下划线代表空格,第一次用markdown,下班时间,凭记忆用的还是..请见谅)
__...
关于第一部分差不多就说这么多了,如果有时间第二部分一定尽快奉上
(由于之前的2B行为,整理了一天git,项目代码放到这里,感兴趣的可以看看)。
I'm Chris, an iOSer. 欢迎讨论,微博@叫Chris真难。:)