初识 Reactive Cocoa

ReactiveCocoa 是一个符合 FRP(Functional Reactive Programming)范式的,操作和转换数据流的框架。

它可以解决三个问题:

  • 开发过程中关于界面的状态信息太多,不易维护。
  • 统一各种消息传递的方式
  • 绑定功能可以配合 MVVM 使用。

基本概念

简而言之,Singal 以 stream 的方式将 event 发送给 Observer。

Event

Event 代表值发生变化,或者有用户操作,分为 4 种:

  • Next:signal 产生了新的值。
  • Failed:signal 执行发生了错误,不再产生新的值。
  • Completed:signal 执行结束,不再产生新的值。
  • Interrupted:signal 被中止,不再产生新的值。

Singal

Signal 类似于一个 event 源头,event 通过 signal 传递。用户只能通过 subscribe 来开始有序地、被动地获取 signal 里面的值。一一个 signal 的生命周期通常是一个或多个 Next event,然后以 Failed、Completed 和 Interrupted 中的一种 event 结束,不受 subscriber 的影响。

用户可以使用基本操作符对 Signal 进行一些操作,如过滤(filter)、映射(map)等。

Observer

通过 subscribe 一个 signal 来接受 event 的对象。

RACCommand

通常用来执行一个 UI 事件发生后要处理的任务。

RACSubject

一个 signal 的子类,是一个可以手动发送 Next、Failed 等事件的 signal。通常用来将 non-RAC 的代码 bridge 成 RAC 的代码。

基本操作

添加副作用

  • -subscribe...:

用来订阅一个 signal 的 Next、Failed、Completed 和 Interrupted 事件中的一种或几种:

1
2
3
4
5
6
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// Outputs: A B C D E F G H I
[letters subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];
  • -do...:

用来添加副作用,当 Next、Failed、Completed 和 Interrupted 事件中的一种发生时执行 do... 中的 block。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__block unsigned subscriptions = 0;

RACSignal *loggingSignal = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
subscriptions++;
[subscriber sendCompleted];
return nil;
}];

// Does not output anything yet
loggingSignal = [loggingSignal doCompleted:^{
NSLog(@"about to complete subscription %u", subscriptions);
}];

// Outputs:
// about to complete subscription 1
// subscription 1
[loggingSignal subscribeCompleted:^{
NSLog(@"subscription %u", subscriptions);
}];

转换 stream

  • -map:

将一个 stream 中的值转换成其他的值,组成一个新的 stream。

1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;

// Contains: AA BB CC DD EE FF GG HH II
RACSequence *mapped = [letters map:^(NSString *value) {
return [value stringByAppendingString:value];
}];
  • -filter:

用来过滤 stream 中的值,传入的 block 返回一个 BOOL,来决定是否将值放入新的 stream 中。

1
2
3
4
5
6
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 2 4 6 8
RACSequence *filtered = [numbers filter:^ BOOL (NSString *value) {
return (value.intValue % 2) == 0;
}];

组合 stream

  • -concat:

将一个 stream 的值追加到另一个 stream 的值后面。

1
2
3
4
5
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *concatenated = [letters concat:numbers];
  • -flatten:

如果一个 signal 的 stream 中的值也是一个 stream,-flatten: 会将子 stream 中的值取出,组成一个新的 stream。组合的效果可能是 concat:

1
2
3
4
5
6
RACSequence *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;
RACSequence *sequenceOfSequences = @[ letters, numbers ].rac_sequence;

// Contains: A B C D E F G H I 1 2 3 4 5 6 7 8 9
RACSequence *flattened = [sequenceOfSequences flatten];

或者 merge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *signalOfSignals = [RACSignal createSignal:^ RACDisposable * (id<RACSubscriber> subscriber) {
[subscriber sendNext:letters];
[subscriber sendNext:numbers];
[subscriber sendCompleted];
return nil;
}];

RACSignal *flattened = [signalOfSignals flatten];

// Outputs: A 1 B C 2
[flattened subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
  • -flattenMap:

当一个 stream 的值也是 stream 时,将子 stream 中的值转换后,组成一个新的 stream。

-flattenMap: = -map: + -flatten:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RACSequence *numbers = [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence;

// Contains: 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
RACSequence *extended = [numbers flattenMap:^(NSString *num) {
return @[ num, num ].rac_sequence;
}];

// Contains: 1_ 3_ 5_ 7_ 9_
RACSequence *edited = [numbers flattenMap:^(NSString *num) {
if (num.intValue % 2 == 0) {
return [RACSequence empty];
} else {
NSString *newNum = [num stringByAppendingString:@"_"];
return [RACSequence return:newNum];
}
}];

组合 signal

  • -then:

当一个 signal 执行完成之后,subscriber 后续只会收到新返回的 signal 的值,如同订阅了新的 signal 一样。

1
2
3
4
5
6
7
8
9
10
11
12
RACSignal *letters = [@"A B C D E F G H I" componentsSeparatedByString:@" "].rac_sequence.signal;

// The new signal only contains: 1 2 3 4 5 6 7 8 9
//
// But when subscribed to, it also outputs: A B C D E F G H I
RACSignal *sequenced = [[letters
doNext:^(NSString *letter) {
NSLog(@"%@", letter);
}]
then:^{
return [@"1 2 3 4 5 6 7 8 9" componentsSeparatedByString:@" "].rac_sequence.signal;
}];
  • +merge:

将多个 signal 合并为一个,一旦任一合并前的 signal 中产生新的值,新的 signal 立即产生同样的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *merged = [RACSignal merge:@[ letters, numbers ]];

// Outputs: A 1 B C 2
[merged subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[numbers sendNext:@"1"];
[letters sendNext:@"B"];
[letters sendNext:@"C"];
[numbers sendNext:@"2"];
  • +combineLatest:+combineLatest:reduce:

将多个 signal 合并成一个。与 +merge: 不同的是,这两个方法产生的新的 signal 只有在所有的合并前 signal 至少产生一个新的值时,才将所有的值(以元组的方式)返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSignal *combined = [RACSignal
combineLatest:@[ letters, numbers ]
reduce:^(NSString *letter, NSString *number) {
return [letter stringByAppendingString:number];
}];

// Outputs: B1 B2 C2 C3
[combined subscribeNext:^(id x) {
NSLog(@"%@", x);
}];

[letters sendNext:@"A"];
[letters sendNext:@"B"];
[numbers sendNext:@"1"];
[numbers sendNext:@"2"];
[letters sendNext:@"C"];
[numbers sendNext:@"3"];
  • -switchToLatest

用来处理由 signal 组成的 signal (signal-of-signals),总是返回最新的外层 signal 中的最新的 signal 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
RACSubject *letters = [RACSubject subject];
RACSubject *numbers = [RACSubject subject];
RACSubject *signalOfSignals = [RACSubject subject];

RACSignal *switched = [signalOfSignals switchToLatest];

// Outputs: A B 1 D
[switched subscribeNext:^(NSString *x) {
NSLog(@"%@", x);
}];

[signalOfSignals sendNext:letters];
[letters sendNext:@"A"];
[letters sendNext:@"B"];

[signalOfSignals sendNext:numbers];
[letters sendNext:@"C"];
[numbers sendNext:@"1"];

[signalOfSignals sendNext:letters];
[numbers sendNext:@"2"];
[letters sendNext:@"D"];

其他

  • take:(NSUInteger)n

仅仅值的前 n 次的变化会继续传递下去。

  • distinctUntilChanged

仅当 signal 的值与上一次不同时才会继续传递下去。

  • throttle:(NSTimeInterval)interval

在 interval 的时间内发生的值的变化仅传递最后一个。

统一消息传递机制

ReactiveCocoa 统一了 iOS 中几乎所有的消息传递方式:

  • Blocks
  • Delegates
  • Notifications
  • Errors
  • Target-Action
  • KVO
  • Method Overriding
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
// Blocks -> Signal

// Groups code that happens on subscriptions, returns a single value when block
// executes. defer is a general pattern to turn a function into a signal.
//
// This can be used to enclose a long-running function into an
// async signal.

[RACSignal defer:^ {
// Test for sending a value and completing
return [RACSignal return:@(arc4random())];
}];

// The same thing as above can be done (more explicitly)
// with createSignal

[RACSignal createSignal:^(id(<RACSubscriber> subscriber) {
// Perform per-subscription side effects.
[subscription sendNext:@(arc4random())];
[subscription sendCompleted];
return nil;
}];

// Blocks -> Signal

// AFNetworking Example:

[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
[manager GET:URLString parameters:params
success:^(AFHTTPRequestOperation *op, id response) {
[subscriber sendNext:response];
[subscriber sendCompleted];
}
failure:^(AFHTTPRequestOperation *op, NSError *e) {
[subscriber:sendError:e];
}];
}];

// Using disposables w/ operations to cancel

[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
NSOperation *operation = [manager GET:URLString parameters:params
success:^(AFHTTPRequestOperation *op, id response) {
[subscriber sendNext:response];
[subscriber sendCompleted];
} failure:^(AFHTTPRequestOperation *op, NSError *error) {
[subscriber sendError:e];
}];
return [RACDisposable disposableWithBlock:^ {
[operation cancel];
}];
}];


// Core Data : switchToLatest on a search request : automatically cancel old requests.

[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACDisposable *disposable = [RACDisposable new];

[managedObjectContext performBlock:^ {
if (disposable.disposed) return;

NSError *error;
NSArray *results = [moc performFetch:fetchRequest error:&error];

if (results != nil) {
[subscriber sendNext:results];
[subscriber sendCompleted];
} else {
[subscriber sendError:error];
}
}];
return disposable;
}];


// Delegates : creating a signal version of a delegate.

// Shows the general pattern of wrapping delegate callbacks into
// signals. Also how to perform side-effect actions based on the
// subscriber count. In this example, CL is turned on when the
// first subscriber subscribes and turned off after all subscribers
// have unsubscribed.
//
// Notice we are setting `self` as the CL delegate but the
// delegate methods are not implemented - rather subscribed
// to via rac_signalForSelector. This translates
// delegate callbacks into signal values.

// reduceEach is like map. In this case, we use it to return only
// the values in the tuple that matter (the locations / error object).

// flattenMap takes a value and creates a signal from it.

CLLocationManager *locationManager = ...
locationManager.delegate = self; //
static volatile int32_t subscriberCount = 0;

[RACSignal createSignal:^(id<RACSubscriber> subscriber) {
RACSignal *locations = [[self rac_signalForSeletor:(@selector(...didUpdateLocations:)
fromProtocol:@protocol(CLLocationManagerDelegate)]
reduceEach^(id _, NSArray *locations) {
return locations;
}];

RACSignal *error = [[self rac_signalForSeletor:(@selector(...didFailWithError:)
fromProtocol:@protocol(CLLocationManagerDelegate)]
reduceEach^(id _, NSError *error) {
return error;
}]
filter:^BOOL (NSError *error) {
// Documentation says CL will keep trying after kCLErrorLocationUnknown
return error.code != kCLErrorLocationUnknown;
}]
flattenMap:^(NSError *error){
return [RACSignal error:error]; // create a new signal that will send error.
}];

RACDisposable *disposable = [[RACSignal
merge:@[ locations, error ]]
subscribe:subscriber];

// manage side effects if you have multiple subscribers
if (OSAtomicIncrement32(&subscriberCount) == 1) {
[locationManager startUpdatingLocation];
} else {
[subscriber sendNext:locationManager.location];
}

return [RACDisposable disposableWithBlock:^{
[disposable dispose];
if (OSAtomicDecrement32(&subscriberCount) == 0) {
[locationManager stopUpdateLocation];
}
}];
}];



// KVO

RACSignal *isReachable = [RACObserve(reachabilityManager, networkReachabilityStatus)
map:^(NSNumber *networkReachabilityStatus) {
switch (networkReachabilityStatus.intValue) {
case AFNetworkReachabilityStatusReachableViaWWAN:
case AFNetworkReachabilityStatusReachableViaWiFi:
return @YES;
}
return @NO;
}];

// Notifications

RACSignal *isForeground = [RACSignal merge:@[
[[defaultCenter rac_addObserverForName:WillEnterForeground ...]
mapReplace:@YES]
[[defaultCenter rac_addObserverForName:DidEnterBackground ...]
mapReplace:@NO]
]];


// Listens to the foreground. When isForeground == @YES, sends values from
// the "didBecomeActive" signal. You can use this from a VM
// to *not* bind to UIApplicationDelegate
RACSignal *hasLaunchedActive = [RACSignal
if:isForeground
then:[defaultCenter rac_addObserverForName:DidBecomeActive]
else:[RACSignal empty]];

UIView Categories

ReactiveCocoa 提供了许多 UIView 的 Category 用来快速将 UI 的事件、属性转换成 signal。例如 rac_textSignal 是 UITextField 的 text 属性转换成的信号;rac_buttonClickedSignal 是 UIAlertView 的按钮点击的信号。

我们订阅这些信号就可以摆脱各种 Delegate。

1
2
3
4
5
6
7
8
9
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"" message:@"Alert" delegate:nil cancelButtonTitle:@"YES" otherButtonTitles:@"NO", nil];
[[alertView rac_buttonClickedSignal] subscribeNext:^(NSNumber *indexNumber) {
if ([indexNumber intValue] == 1) {
NSLog(@"NO");
} else {
NSLog(@"YES");
}
}];
[alertView show];

ReactiveCocoa 提供了一些非常方便的宏。

  • RACObserve(self, status):将 status 转换成一个 signal,当其值发生变化时触发事件。作用与 KVO 相同。
  • RAC(self.textField, text):将某个对象的某个属性绑定到一个 signal 的值上。
  • @weakify(self)@strongify(self) 用于避免循环引用。

Ref

给鸡排饭加个蛋