ReactiveCocoa Introduction(1/2)

ReactiveCocoa Tutorial – The Definitive Introduction: Part 1/2 的学习笔记

Part 2/2 先放一下,以后再研究。

iOS开发目前所用的事件(Events)以及对应的响应模型:target-actions, delegates, KVO, callbacks 等等,各不一样。

ReactiveCocoa则定义了一个事件的标准接口,使得它们更方便地串联、过滤以及组合。使用如下概念

a)Functional Programming(将函数提升到first-class地位,可以把函数作为传递的参数)

b)Reactive Programming 专注于数据流的传播和改变

所以Reactive Programming也叫Functional Reactive Programming(FRP) framework

再来一个概念, fluent interface,  类似于连续赋值的的method cascading/method chaining

c++的例子(wiki)如下, 每个成员函数都返回this的引用的基础上,最终使用接口的代码可以写成如下形式:

 // Fluent usage
 int main(int argc, char **argv) {
     FluentGlutApp(argc, argv)
         .withDoubleBuffer().withRGBA().withAlpha().withDepth()
         .at(200, 200).across(500, 500)
         .named("My OpenGL/GLUT App")
         .create();
 }

(Objective-C这样连续调用,带来的不好看的一面是会在最前面累计无数"[")

以上具体概念可查阅wiki站,以下为教学内容,本次实例是将一个登录验证界面由传统的改为Reactive的

1. rac_textSignal 生成一个RACSignal 作为起点向它们的subscriber发送一系列事件,分类为三种: next, error, completed.

比如下面的subscribeNext 即是next事件。这句完成的动作是,usernameTextField的内容一旦被改变,即调用subscribeNext:注册的block,参数为textField.text

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

2.  filter: 来支持条件过滤

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

3. map: 来转义(否则每次传出来的都是同一个类型, 这个例子里是NSString*)

4. 用RAC(id, property)创建一个RACSignal的例子, validPasswordSignal参考上面。到这步,实现了输入字母够长则改变输入框背景颜色的功能。

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

5. 合并信号combineLatest:, 用户名跟密码都合法了才能进行下一步操作(显示登录摁钮)

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

6. 对于button的改造,准备登录

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

7. 改造一个dummy service(判断输入是否合法,通过则进入下一屏) 于是

-(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;
  }];
}

createSignal丢进来的是这样一个东西(block): (RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe

它是一个block, 返回值是RACDisposable *(大概是最后回收内存等清理工作?), block的参数是实现了RACSubscriber protocol的NSObject*

所以上面代码倒数第三行需要return nil, nil表示不需要清理。block内部,执行完判断后,给subscriber发送next 跟completed 事件。

8. signal of signals

参考下面的代码,如果不是flattenMap: 而是用map:, 运行时出错,丢出来的result: x是RACDynamicSignal

不是期待的@(success)合成的0或者1。 改完后,就可以在subscribeNext的block中根据结果做对应的操作了。

[[[self.signInButton
   rac_signalForControlEvents:UIControlEventTouchUpInside]
   flattenMap:^id(id x) { // x = signButton
     return [self signInSignal];
   }]
   subscribeNext:^(id x) { // x = the signal if not flatternMap
     NSLog(@"Sign in result: %@", x);
   }];

9. 副作用/side effect

很细心的一个细节: 用户摁了signin button后必须disable掉这个button防止重复摁(其实是另一个细节带来的,为了防止暴力测试,验证过程中会delay2s出结果,在这个过程中,需要阻止多次sign in请求)。 从signal的角度看,这个要求是side effect, 它并不改变事件本身。

所以引入 doNext: , 最后代码是这样的:

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

 

结论是ReactiveCocoa的代码逻辑性非常强,什么条件触发什么动作一目了然,避免了维护一堆记录状态的变量。确实炫。类似的入门描述还可以参考

http://www.teehanlax.com/blog/reactivecocoa/

http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/

这个教程的第二部分,是描述另外两种事件类型: error跟completed, 以及throttling, threading,  continuations的,暂时感觉距离目前代码有点遥远,不能直接利用,先搁置吧。

2014.04.11

posted on 2014-04-11 16:23  kurk  阅读(671)  评论(0编辑  收藏  举报

导航