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...大概设计成这样

Reactive Cocoa 之旅_第1张图片
简陋的原型

界面比较简单,一个常规的注册页面。要求:用户名长度必须大于3,密码长度大于5,否则textField背景色为黄色,正确则为白色;密码框内的内容要和确认密码框的内容一致,否则确认密码框的背景色为黄色;若上述条件都满足,注册按钮可以点击,否则不可点击。

规则简单,也很合理,那么我们需要怎么做呢?

这个简单,看我的:

方案1(With out RAC):

Storyboard中大概这样:


Reactive Cocoa 之旅_第2张图片
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

思路很简单:

  1. userNameField逻辑最简单,根据用户名的长度,修改其输入框背景颜色,判断注册按钮的enable状态,对其更新;
  2. 用户输入的密码如果符合规则,修改passwordField背景色。同时,如果confirmField符合规则,更新confirmField的背景色,判断注册按钮的enable状态,对其更新;
  3. 判断confirmField的文字,修改其背景色,判断注册按钮的enable状态,对其更新;
  4. 点击signUpButton,获得龙虾一只。

可以发现,在每一个TextField文本改变的时候,我们都要去判断并更新signUpButton的enable状态,并且在编辑passwordField文本的时候,判断并更新confirmField的背景色;

来看看如果使用RAC怎么搞呢?

方案2 (With in RAC) :

#import "ViewController.h"
#import 

@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之类的东西。我大概的思路是这样的:

  1. 观察userNameField的rac_textSignal(RAC对许多类都有其不同的Signal,如果感兴趣可以去看一看),textSignal、textSignal,顾名思义,一个文本事件流,发出的信号是一个NSString *类型的对象,然后通过map:方法将信号转换成一个BOOL含义的NSNumber标识(我们发出、转换或者合并的信号量都是NSObject类型,如果需要返回基本类型,例如此处的BOOL类型,需要将基本类型升级成对象,例如此处的NSNumber *),然后通过subscirbeNext:方法订阅(个人理解为接收一个事件流,事件流发出的信号,在订阅期间可以接收到)这个信号,如果接收到的信号为真,代表合法的用户名,改变userNameField背景色为白色,否则背景色为黄色。passwordField同理;
  2. confirmField只有在密码合法并且其文本与passwordField的文本相同时,才改变其颜色为白色,否则为黄色,用RAC该怎么处理呢?在这里,我选择了combine(组合)的方式。
  • 将confirmField.rac_textSignal与passwordSignal通过combineLatest:方法组合成一个新的事件流confirmSignal,也就是说,用户每次编辑passwordField或者confirmField的时候,我们都能接收到confirmSignal发出BOOL含义的NSNumber *信号,然后订阅这个事件流,通过信号来更新其背景色;
  1. 那么更新signUpButton的逻辑同上,将三个事件流组合成一个新的事件流,订阅事件流,根据信号来更新enable状态;
  2. 事件流的处理到此为止,接下来,我们要点击注册按钮的时候,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真难。:)

你可能感兴趣的:(Reactive Cocoa 之旅)