ReactiveCocoa Novice Guide

看了很多关于ReactiveCocoa的文章,总结成一句:ReactiveCocoa(RAC) 是来拯救我们的。

RACSignal

RACSignal (信号)就 RAC 来说是构造单元。它代表我们最终将要收到的信息,当你能将未来某时刻收到的消息具体表示出来时, 你可以开始预先(陈述性)运用逻辑并构建你的信息流,而不是必须等到事件发生(命令式).

信号会为了控制通过应用的信息流而获得所有这些异步方法(委托, 回调 block, 通知, KVO, target/action 事件观察, 等)并将它们统一到一个接口下.这只是直观理解. 不仅是这些, 因为信息会流过你的应用, 它还提供给你轻松转换/分解/合并/过滤信息的能力.

那么什么是信号呢?

信号是一个发送一连串值的物体. 但是我们这儿的信号啥也不干, 因为它还没有订阅者. 如果有订阅者监听时(已订阅)信号才会发信息. 它将会向那个订阅者发送0或多个载有数值的”next”事件, 后面跟着一个”complete”事件或一个”error”事件.

信号发送的值是从哪获得的?

信号是一些等待某事发生的异步代码, 然后把结果值发送给它们的订阅者. 你可以用 RACSignal 的类方法 createSignal: 手动创建信号:

1
2
3
4
5
6
7
8
9
10
//networkSignal.m
RACSignal *networkSignal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber){
NetworkOperation *operation = [NetworkOperation getJSONOperationForURL:@"http://someurl"];
[operation setCompletionBlockWithSuccess:^(NetworkOperation *theOperation, id *result) {
[subscriber sendNext:result];
[subscriber sendCompleted];
} failure:^(NetworkOperation *theOperation, NSError *error) {
[subscriber sendError:error];
}];
}];

我在这用一个具有成功和失败 block (伪造)的网络操作创建了一个信号. (如果我想让信号在被订阅时才让网络请求发生, 还可以用 RACSignal 的类方法 defer. )我在成功的 block 里使用提供的 subscriber 对象调用 sendNext:sendCompleted: 方法, 或在失败的 block 中调用 sendError:. 现在我可以订阅这个信号并将在响应返回时接收到 json 值或是 error.

  • next:是可以为nil的新值, RACStream方法只能在这个值上进行操作运算。
  • error:表示在Signals完成之前发生了错误,值不会在RACStream类中存储。
  • completed:表示Signals成功的完成,值不会在RACStream类中存储。

幸运的是, RAC 的创造者实际上使用它们自己的库来创建真的事物(捉摸一下), 所以对于我们在日常需要什么, 他们有很强烈的想法. 他们为我们提供了很多机制, 来从我们通常使用的现存的异步模式中拉取信号. 别忘了如果你有一个没有被某个内建信号覆盖到的异步任务, 你可以很容易地createSignal: 或类似方法来创建信号.

一个被提供的机制就是 RACObserve() 宏. (如果你不喜欢宏, 你可以简单地看看罩子下面并用稍微多些冗杂的描述. 这也非常好. 在我们得到 Swift 版本的替代之前, 这也有在 Swift 中使用 RAC 的解决方案. )这个宏是 RAC 中对 KVO 中那些悲惨的 API 的替代. 你只需要传入对象和你想观察的那个对象某属性的 keypath. 给出这些参数后, RACObserve 会创建一个信号, 一旦它有了订阅者, 它就立刻发送那个属性的当前值, 并在发送那个属性在这之后的任何变化.

1
RACSignal *usernameValidSignal = RACObserve(self.viewModel, usernameIsValid);

这仅是提供用于创建信号的一个工具. 这里有几个立即可用的方式, 来从内置控制流机制中拉取信号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//signals.m
RACSignal *controlUpdate = [myButton rac_signalForControlEvents:UIControlEventTouchUpInside];
// signals for UIControl events send the control event value (UITextField, UIButton, UISlider, etc)
// subscribeNext:^(UIButton *button) { NSLog(@"%@", button); // UIButton instance }
RACSignal *textChange = [myTextField rac_textSignal];
// some special methods are provided for commonly needed control event values off certain controls
// subscribeNext:^(UITextField *textfield) { NSLog(@"%@", textfield.text); // "Hello!" }
RACSignal *alertButtonClicked = [myAlertView rac_buttonClickedSignal];
// signals for some delegate methods send the delegate params as the value
// e.g. UIAlertView, UIActionSheet, UIImagePickerControl, etc
// (limited to methods that return void)
// subscribeNext:^(NSNumber *buttonIndex) { NSLog(@"%@", buttonIndex); // "1" }
RACSignal *viewAppeared = [self rac_signalForSelector:@selector(viewDidAppear:)];
// signals for arbitrary selectors that return void, send the method params as the value
// works for built in or your own methods
// subscribeNext:^(NSNumber *animated) { NSLog(@"viewDidAppear %@", animated); // "viewDidAppear 1" }

什么是订阅者?

简言之, 订阅者就是一段代码, 它等待信号给它发送一些值, 然后订阅者就能处理这些值了. (它也可以作用于 “complete” 和 “error” 事件. )

这有一个简单的订阅者, 是通过向信号的实例方法 subscribeNext 传入一个 block 来创建的. 我们在这通过 RACObserve() 宏创建信号来观察一个对象上属性的当前值, 并把它赋值给一个内部属性.

1
2
3
4
5
6
7
8
9
- (void) viewDidLoad {
// . . .
// create and get a reference to the signal
RACSignal *usernameValidSignal = RACObserve(self.viewModel, isUsernameValid);
// update the local property when this value changes
[usernameValidSignal subscribeNext: ^(NSNumber *isValidNumber) {
self.usernameIsValid = isValidNumber. boolValue
}];
}

注意 RAC 只处理对象, 而不处理像 BOOL 这样的原始值. 不过不用担心, RAC 通常会帮你这些转换.

幸运的是 RAC 的创造者也意识到这种绑定行为的普遍必要性, 所以他们提供了另一个宏 RAC(). 与 RACObserve() 相同, 你提供想要与即将到来的值绑定的对象和参数, 在其内部它所做的是创建一个订阅者并更新其属性的值. 我们的例子现在看起来像这样:

1
2
3
4
- (void) viewDidLoad {
//. . .
RAC(self, usernameIsValid) = RACObserve(self.viewModel, isUsernameValid);
}

考虑下我们的目标, 这么干有点傻啊. 我们不需要将信号发送的值存到属性中(这会创建状态), 我们真正要做的是用从那个值获取到信息来更新 UI.

转换数据流

现在我们进入 RAC 为我们提供的用于转换数值流的方法. 我们将会利用 RACSignal 的实例方法 map.

1
2
3
4
5
6
7
8
9
10
//transformingStreams.m
- (void) viewDidLoad {
//...
RACSignal *usernameIsValidSignal = RACObserve(self.viewModel, isUsernameValid);
RAC(self.goButton, enabled) = usernameIsValidSignal;
RAC(self.goButton, alpha) = [usernameIsValidSignal
map:^id(NSNumber *usernameIsValid) {
return usernameIsValid.boolValue ? @1.0 : @0.5;
}];
}

这样现在我们将 view-model 上的 isUsernameValid 发生的变化直接绑定到 goButtonenabled 属性上. 酷吧?对 alpha的绑定更酷, 因为我们正在使用 map 方法将值转换成与 alpha 属性相关的值. (注意在这里我们返回的是一个 NSNumber 对象而不是原始float值. 这基本上是唯一的污点: 你需要负责为 RAC 将原始值转化为对象, 因为它不能帮你导出来.

RAC常用宏

RACObserve(TARGET, KEYPATH)

表现形式:RACObserve(self, stringProperty)

KVO的简化版本 相当于对TARGET中KEYPATH的值设置监听,返回一个RACSignal

RAC(TARGET, …)

表现形式:RAC(self, stringProperty) = TextField.rac_textSignal

第一个是需要设置属性值的对象,第二个是属性名

RAC宏允许直接把信号的输出应用到对象的属性上

每次信号产生一个next事件,传递过来的值都会应用到该属性上

RACChannelTo(TARGET, …)

RACChannelTo 用于双向绑定

RACChannelTo(self, stringProperty)=RACChannelTo(self.label, text) ;

RAC 双向绑定实现案例

RAC基础使用

subscribeNext

1
2
3
4
[self.usernameTextField.rac_textSignal
subscribeNext:^(id x){ // 设置订阅 监听TextField
NSLog(@"%@", x);
}];

filter

过滤器,可以添加一些筛选条件。

1
2
3
4
[self.usernameTextField.rac_textSignal
filter:^BOOL(NSString* text){
return text.length > 3;
}];

map

使用map可以对信号进行转换,一个源信号转换成另外一个新的信号输出。

1
2
3
4
5
6
7
8
9
10
[[[self.usernameTextField.rac_textSignal
map:^id(NSString*text){
return @(text.length);
}]
filter:^BOOL(NSNumber*length){
return[length integerValue] > 3;
}]
subscribeNext:^(id x){
NSLog(@"%@", x);
}];

聚合(combineLatest: reduce:)

多个信号可以聚合成一个新的信号,这个可以是任何类型的信号.

1
2
3
4
RAC(self.loginButton, enabled) = [RACSignal combineLatest:@[self.userNameFeild.rac_textSignal, self.passwordFeild.rac_textSignal]
reduce:^(NSString *username, NSString *password) {
return @0;
}];

分割

一个信号可以有很多subscriber,也就是作为很多后续步骤的源.

1
2
3
4
5
6
7
RACSignal *signal = self.usernameTextField.rac_textSignal;
[signal subscribeNext:^(id x) {
NSLog(@"1111");
}];
[signal subscribeNext:^(id x) {
NSLog(@"2222");
}];

Button的ControlEvents

1
2
3
4
5
[[self.signInButton
rac_signalForControlEvents:UIControlEventTouchUpInside]
subscribeNext:^(id x) {
NSLog(@"button click");
}];

doNext

为一个附加操作,在一个next事件发生时执行的逻辑,而该逻辑并不改变事件本身。

1
2
3
4
5
[[_login rac_signalForControlEvents:UIControlEventTouchUpInside]
doNext:^(id x) {
// 点击按钮时执行其他逻辑
}
];

flattenMap

信号的FlattenMap与Map

避免循环引用

在ReactiveCocoa中提供了避免循环引用的方法

  • @weakify宏让你创建一个弱引用的影子对象(如果你需要多个弱引用,你可以传入多个变量),
  • @strongify让你创建一个对之前传入@weakify对象的强引用。
1
2
3
4
5
6
7
8
9
10
@weakify(self)
[[self.searchText.rac_textSignal
map:^id(NSString *text) {
return [self isValidSearchText:text] ?
[UIColor whiteColor] : [UIColor yellowColor];
}]
subscribeNext:^(UIColor *color) {
@strongify(self)
self.searchText.backgroundColor = color;
}];

RAC使用场景

网络请求

待续

实时搜索内容方法

待续

节流

待续

参考文献

ReactiveCocoa框架菜鸟入门

ReactiveCocoa(RAC)-iOS

说说ReactiveCocoa 2

学习RAC小记-适合给新手看的RAC用法总结

ReactiveCocoa-tutorial-pt1

ReactiveCocoa-tutorial-pt2

MVVC-RAC

iOS 基于MVVM + RAC + ViewModel-Based Navigation的微信开发(一)