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