1

(这篇文章原来发布在 csdn ,现在 blog 迁移过来,并用 Markdown 重新排版以及修改)

本文英文原文出自这篇文章 ,但我只是有选择性的进行了翻译。

rac 强调原子操作以及组装。rac 基本上是建立在信号的基础上的,也就是 RACSignal ,所有的操作都能转成 RACSignal 来组装操作。这篇文章主要从信号的角度进行介绍。

单个信号

rac入门最经典的一个例子就是一个登录界面,如下:
图片描述
要求只有当用户名和密码都满足的时候,高亮 sign in 按钮。

要想实现这样的功能,传统的做法,你需要在 delegate 里面监听输入文字的变化,并做校验,这样至少同一个逻辑的代码是分散开的,而且还需要写很多额外代码,rac 里一个很大的特色就是能够将让代码不分,rac 实现上面的功能将会很简介,代码如下:

[self.usernameTextField.rac_textSignal subscribeNext:^(id x) {
  NSLog(@"%@", x);
}];

一行代码足矣。。。其中 self.usernameTextField.rac_textSignal 就是一个RACSignal , RAC 对许多基础组建都封装了 RACSignal ,并不需要我们自己去创建。运行上面代码,然后在 username 输入框中连续输入 3 个 d,输出如下

2016-02-19 20:37:42.309 ReactiveExample[71930:6364937] d
2016-02-19 20:37:42.582 ReactiveExample[71930:6364937] dd
2016-02-19 20:37:42.952 ReactiveExample[71930:6364937] ddd

是不是很简单!

不仅如此,如果你还想对用户名进行校验,比如要求用户名的长度大于3,那么,你只需要将上面的代码改为如下即可:

RACSignal *filteredUsername = [usernameSourceSignal
                                   filter:^BOOL(id value) {
                                       NSString *text = value;
                                       return text.length > 3;
                                   }];
    
    [filteredUsername subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];

输出如下

2016-02-19 20:50:42.069 ReactiveExample[72046:6445227] dddd
2016-02-19 20:50:43.530 ReactiveExample[72046:6445227] ddddd
2016-02-19 20:50:44.858 ReactiveExample[72046:6445227] dddddd

当输入的字符个数大于3的时候,才会触发输出。是不是很神奇!

不过在继续了解之前,我需要向大家简单介绍 RAC 中两个基本的概念

  1. 信号,也就是 RACSignal 对象。

  2. 订阅,如上面的 subscribeNext操作。

如下代码

[filteredUsername subscribeNext:^(id x) {
        NSLog(@"%@", x);
    }];

filteredUsername是一个信号,subscribeNext 表示对信号 filteredUsername 进行了订阅。这样写的结果是,当 filteredUsername 发出信号的时候,就会被订阅者感知到。因此会输出log。

rac 基本方法

filter

在上面在做长度大于3的判断时,我们用到了 filter 操作。filter是一个 rac 操作,它的作用是将满足条件的usernameSourceSignal信号转化成了filteredUsername信号,rac 有非常多这种操作,有兴趣的可以查看起官网文档。当然上面的代码,你也可以组合在一起,如下

[self.usernameTextField.rac_textSignal
     filter:^BOOL(NSString *text) {
         return text.length > 3;
     }]
subscribeNext:^(id x) {
    NSLog(@"%@", x);
}];

map

上面的 filter 只是一个过滤操作,其实产生的新信号 filteredUsername 本质上还是usernameSourceSignal,只不过是满足一定条件的 usernameSourceSignal 。在rac中,你完全可以将一个信号转化成一个完成不同的信号。见如下代码

 [[[self.usernameTextField.rac_textSignal
       map:^id(NSString *text) {
           return @(text.length);
       }]
      filter:^BOOL(NSNumber *length) {
          return [length integerValue] > 3;
      }]
     subscribeNext:^(id x) {
         NSLog(@"%@", x);
     }];

跟上面只有filter进行同样的输入,输出结果如下

2016-02-19 21:03:14.344 ReactiveExample[72125:6500152] 4
2016-02-19 21:03:15.112 ReactiveExample[72125:6500152] 5
2016-02-19 21:03:15.806 ReactiveExample[72125:6500152] 6

注意对比会发现,这里输出的不再是输入的 dddd ddddd dddddd,而是 d 的个数了,现在已经是一个完全不同的信号了。这是因为我们对 self.usernameTextField.rac_textSignal 进行了 map 操作,形成新的信号,而这个信号传递的是@(text.length),事实上,这里我们可以传递任何对象。

两个信号

上面只考虑了单个信号的情况,现在我们考虑两个信号的情况,见如下代码:

RACSignal *validUsernameSignal =
  [self.usernameTextField.rac_textSignal
    map:^id(NSString *text) {
      return @([self isValidUsername:text]);
    }];
 
RACSignal *validPasswordSignal =
  [self.passwordTextField.rac_textSignal
    map:^id(NSString *text) {
      return @([self isValidPassword:text]);
    }];

现在我们做一个考虑,当用户名,或者密码正确的时候,输入框显示 clearColor 否则显示 yellowcolor。

单独来看,比如只是对密码做上诉校验,也就是当输入密码的时候,输入框根据输入密码的对错显示不同的颜色,代码如下:

[[validPasswordSignal
  map:^id(NSNumber *passwordValid) {
    return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
  }]
  subscribeNext:^(UIColor *color) {
    self.passwordTextField.backgroundColor = color;
  }];

如果同时当将两者考虑在一起,可以如下

RAC(self.passwordTextField, backgroundColor) =
  [validPasswordSignal
    map:^id(NSNumber *passwordValid) {
      return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
    }];
 
RAC(self.usernameTextField, backgroundColor) =
  [validUsernameSignal
    map:^id(NSNumber *passwordValid) {
     return [passwordValid boolValue] ? [UIColor clearColor] : [UIColor yellowColor];
    }];

上面的 RAC() 是 RAC 框架提供的一种宏,用于将信号的输出直接赋值给绑定的对象。

这里还是单独处理的,rac 的一大特点就是组装。接下来介绍怎么将这两个信号绑定到一起。回到最初的需求,我们需要在当用户名以及密码同时有效的情况下,高亮 sign in 按钮。这里需要用到 combine 操作。如下

RACSignal *signUpActiveSignal =
  [RACSignal combineLatest:@[validUsernameSignal, validPasswordSignal]
                    reduce:^id(NSNumber *usernameValid, NSNumber *passwordValid) {
                      return @([usernameValid boolValue] && [passwordValid boolValue]);
                    }];

combineLatest 的作用是将最近的 validUsernameSignal 以及 validPasswordSignal 信号结合起来。reduce 操作将这 combineLatest 起来的两个信号结合成一个信号,这个信号传递的值,可以根据这两个信号分别发出的信号结合起来,组成一个新的值。因此,sign in 按钮的高亮可以根据如下方法来实现

[signUpActiveSignal subscribeNext:^(NSNumber *signupActive) {
   self.signInButton.enabled = [signupActive boolValue];
 }];

当 sign in 按钮高亮的时候,就可以开始处理 sign in 按钮的响应了,响应代码如下

[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   subscribeNext:^(id x) {
     NSLog(@"button clicked");
   }];

自己创建信号

前面使用的信号都是 RAC 框架自带的,很多时候,我们也需要创建属于自己的信号,创建信号如下

-(RACSignal *)signInSignal {
  return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
    [self.signInService
     signInWithUsername:self.usernameTextField.text
     password:self.passwordTextField.text
     complete:^(BOOL success) {
       [subscriber sendNext:@(success)];
       [subscriber sendCompleted];
     }];
    return nil;
  }];
}

这时,sign in 按钮的响应代码替换为

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   map:^id(id x) {
     return [self signInSignal];
   }]
   subscribeNext:^(id x) {
     NSLog(@"Sign in result: %@", x);
   }];

flattenmap

当 sign in 按钮被点击的时候,signInButton 会发生一个信号,注意,在以前,我们只传递了对象,而这里传递了一个信号,会有什么不同呢,这里不会输出一个值,而是会输出一串地址,因为,这里属于信号的信号,而不是普通的信号,为了正常输出,我们需要使用flattenmap,修改如下

[[[self.signInButton
  rac_signalForControlEvents:UIControlEventTouchUpInside]
  flattenMap:^id(id x) {
    return [self signInSignal];
  }]
  subscribeNext:^(NSNumber *signedIn) {
    BOOL success = [signedIn boolValue];
    self.signInFailureText.hidden = success;
    if (success) {
      [self performSegueWithIdentifier:@"signInSuccess" sender:self];
    }
  }];

flattenmap 与 map 的区别在于,flattenmap 会讲 signal 里面的值取出来,形成一个正常的 signal ,而 map 操作,如果碰到一个 signal 对象,它只是简单的将signal 最为一个新的 signal 的值封装成一个信号的信号。


zachwang
53 声望4 粉丝

ios客户端工程师