- Published on
ReactiveCocoa Operation Methods
ReactiveCocoa Operation Prerequisites
All signals (RACSignal) can be processed with operation methods, because all operation methods are defined in RACStream.h. Any class that inherits from RACStream gains access to these operation methods.
ReactiveCocoa Operation Philosophy
RAC uses a Hook pattern. Hook is a technique for changing the execution result of an API (application programming interface or method).
-
Hook's purpose: intercepting API calls.
-
Hook's principle: before each API call returns its result, your own method runs first to modify the output.
ReactiveCocoa Core Method: bind
The core operation method in ReactiveCocoa is bind. In fact, the core development pattern in RAC is binding. Traditional development is assignment-based, whereas RAC development should focus on binding — you can bind the desired behavior at the time an object is created, rather than waiting until after assignment.
For example: instead of overriding a control's setModel method to display data, with RAC you can bind the data at the moment the control is created.
In practice, bind is rarely used directly. It is a low-level method in RAC, and RAC provides many convenient higher-level methods that wrap it internally.
Using bind
Suppose you want to monitor a text field's content and prepend the string "Output:" to every value before it is emitted.
Method 1: Append text after receiving the result.
[self.textField.rac_textSignal subscribeNext:^(id x) {
NSLog(@"Output:%@",x);
}];
2016-03-21 12:03:46.423 RACDemo[46861:5429371] Output:
2016-03-21 12:03:47.610 RACDemo[46861:5429371] Output:a
Method 2: Prepend text before the result is returned, using RAC's bind method.
-
bindparameter: takes a block that returns aRACStreamBindBlock. -
RACStreamBindBlockis a block type that returns a signal and takes parameters (value,stop). So the block parameter itself also returns a block.
RACStreamBindBlock:
-
Parameter 1 (
value): The original unprocessed value received from the signal. -
Parameter 2 (
stop): Controls the bind block. If*stop = YES, binding ends. -
Return value: A signal. Process the data and return it via this signal. Typically uses
RACReturnSignal— requires manually importingRACReturnSignal.h.
Steps for using bind:
-
Pass in a block that returns
RACStreamBindBlock. -
Describe a
bindBlockof typeRACStreamBindBlockas the block's return value. -
Describe a result signal as the
bindBlock's return value.
Note: handle signal result processing inside
bindBlock.
Internals:
-
Calling
bindon the source signal creates a new bound signal. -
When the bound signal is subscribed to, its
didSubscribeis called, generating abindingBlock. -
When the source signal emits a value, it is passed to
bindingBlockfor processing viabindingBlock(value, stop). -
bindingBlock(value, stop)returns a signal (RACReturnSignal) with the processed content. -
Subscribing to
RACReturnSignalretrieves the bound signal's subscriber and sends out the processed content.
Note: different subscribers store different
nextBlocks. When reading source code, be careful to identify which subscriber is which.
Note: you must manually import
#import <ReactiveCocoa/RACReturnSignal.h>to useRACReturnSignal.
[[self.textField2.rac_textSignal bind:^RACStreamBindBlock{
// When is this called:
// block purpose: indicates that a signal has been bound.
return ^RACStream *(id value, BOOL *stop){
// When is this block called: whenever the signal emits a new value.
// block purpose: process the return value
// Process the data and return it via a signal.
return [RACReturnSignal return:[NSString stringWithFormat:@"Output:%@",value]];
};
}] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 12:35:21.044 RACDemo[46918:5435256] Output:
2016-03-21 12:35:23.008 RACDemo[46918:5435256] Output:q
Map
-
flattenMapandMapare used to map the contents of a source signal to new values. For example, you can monitor a text field's content and remap the output to a new value. -
flattenMap: Maps the content of a source signal into a new signal. The signal can be of any type.
flattenMap usage steps:
-
Pass in a block with return type
RACStreamand parametervalue. -
valueis the source signal's content. Use it to do your processing. -
Wrap the result into a
RACReturnSignaland return it.
flattenMap internals:
-
flattenMapis implemented usingbindinternally. The block's return value inflattenMapbecomes the return value ofbindBlockinbind. -
When the bound signal is subscribed to,
bindBlockis generated. -
When the source signal sends content,
bindBlock(value, *stop)is called. -
bindBlockinternally callsflattenMap's block, which wraps the processed data into a signal. -
The returned signal ultimately serves as the return signal of
bindBlock. -
Subscribing to
bindBlock's return signal retrieves the bound signal's subscriber and sends out the processed content.
[[self.textField.rac_textSignal flattenMap:^RACStream *(id value) {
// When is this called: when the source signal emits a value.
// block purpose: transform the source signal's content.
NSLog(@"%@", value);
NSString *newValue = [NSString stringWithFormat:@"new:%@", value];
// Return value: the content for the bound signal.
return [RACReturnSignal return:[NSString stringWithFormat:@"Output:%@", newValue]];
}] subscribeNext:^(id x) {
// Subscribing to the bound signal — called after processing whenever the source signal emits content.
NSLog(@"%@", x);
}];
2016-03-21 12:52:05.323 RACDemo[46971:5439654] a
2016-03-21 12:52:05.324 RACDemo[46971:5439654] Output:new:a
-
Mapusage: for example, monitor a text field's content and remap the output to a new value. -
Map: Maps the source signal's value to a new value. -
Map usage steps:
- Pass in a block that returns an object and takes a
valueparameter. valueis the source signal's content — use it directly.- Return the processed content directly. No need to wrap it in a signal.
- Map internals:
Mapinternally callsflattenMap. The return value fromMap's block becomes the value inflattenMap's block.- When the bound signal is subscribed to,
bindBlockis generated. - When the source signal sends content,
bindBlock(value, *stop)is called. bindBlockinternally callsflattenMap's block.flattenMap's block callsMap's block and wraps its return value into a signal.- The returned signal ultimately serves as the return signal of
bindBlock. - Subscribing to
bindBlock's return signal retrieves the bound signal's subscriber and sends out the processed content.
[[self.textField2.rac_textSignal map:^id(id value) {
// Called when the source signal fires; use this to modify the source signal's content.
// Return value: the processed content from the source signal.
NSLog(@"%@", value);
NSString *newValue = [NSString stringWithFormat:@"new:%@", value];
return [NSString stringWithFormat:@"Output:%@", newValue];
}] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 12:59:53.931 RACDemo[46990:5441308] d
2016-03-21 12:59:53.931 RACDemo[46990:5441308] Output:new:d
Difference between FlatternMap and Map
- The block in
FlatternMapreturns a signal. - The block in
Mapreturns an object. - In practice, if the signal's emitted value is not itself a signal, use
Map. - In practice, if the signal's emitted value is a signal (signal of signals), use
FlatternMap.
Summary: use FlatternMap for signal of signals.
// Create a signal of signals
RACSubject *signalOfsignals = [RACSubject subject];
RACSubject *signal = [RACSubject subject];
[[signalOfsignals flattenMap:^RACStream *(id value) {
// Called only when signalOfsignals emits a signal
return value;
}] subscribeNext:^(id x) {
// Only called when signalOfsignals' signal emits a value, because internally it subscribes
// to the signal returned by bindBlock — i.e., the signal returned by flattenMap.
// So it's only called when flattenMap's returned signal emits content.
NSLog(@"%@ aaa", x);
}];
// signal of signals sends a signal
[signalOfsignals sendNext:signal];
// signal sends content
[signal sendNext:@1];
ReactiveCocoa Combination Operations
concat
-
Concatenates signals in order. When multiple signals are combined, values are received in sequence.
-
The next signal is subscribed to only after the previous one completes. If the previous one errors, the next one is not subscribed.
-
concatinternals:
- When the concatenated signal is subscribed, its
didSubscribeis called. didSubscribefirst subscribes to the first source signal (signalA).- signalA's
didSubscribeis executed. - When signalA's
didSubscribesends values, signalA's subscriber'snextBlockis called and the values are forwarded via the concatenated signal's subscriber. - When signalA's
didSubscribesends a completion, signalA's subscriber'scompletedBlockis called and signalB is subscribed, activating it. - signalB's
didSubscribeis executed. - When signalB's
didSubscribesends values, they are forwarded via the concatenated signal's subscriber.
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
// Concatenate signalB after signalA; signalB is only activated after signalA completes.
RACSignal *concatSignal = [signalA concat:signalB];
// From here on, just work with the concatenated signal.
// No need to subscribe to signalA and signalB separately — the internals handle it automatically.
// Note: the first signal must send a completion before the second signal is activated.
[concatSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 13:12:18.470 RACDemo[47025:5444568] 1
2016-03-21 13:12:18.473 RACDemo[47025:5444568] 2
then
-
Used to connect two signals. The second signal (returned by
then) is connected only after the first signal completes. -
Note: values from the first signal are discarded when using
then. -
Internals:
- First filters out values emitted by the previous signal.
- Then uses
concatto connect the signal returned bythen.
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}] then:^RACSignal *{
return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
}] subscribeNext:^(id x) {
// Only receives values from the second signal — the one returned by then
NSLog(@"%@", x);
}];
2016-03-21 13:23:05.245 RACDemo[47070:5447548] 2
merge
-
Merges multiple signals into one. Any signal emitting a new value triggers the merged signal.
-
All signals are subscribed simultaneously; any response triggers a response.
-
Internals:
- When the merged signal is subscribed, it iterates over all signals and subscribes to each.
- Each signal is subscribed as it is enumerated.
- As soon as the merged signal is subscribed, all internal signals are subscribed.
- Any signal emitting a value is immediately observed.
//Create multiple signals
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
// Merge signals — any signal emitting data will be observed.
RACSignal *mergeSignal = [signalA merge:signalB];
[mergeSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
break;
2016-03-21 13:25:50.674 RACDemo[47080:5448275] 1
2016-03-21 13:25:50.678 RACDemo[47080:5448275] 2
zipWith
-
Zips two signals into one. Only when both signals have emitted a value simultaneously are they combined into a tuple and emitted as the zip signal's next event.
-
Internals:
- Defining the zip signal automatically subscribes to signalA and signalB internally.
- Whenever signalA or signalB emits a value, it checks whether both have emitted. If so, the most recent values from each are wrapped into a tuple and emitted.
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
// Zip signal A and signal B
RACSignal *zipSignal = [signalA zipWith:signalB];
[zipSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 13:32:06.477 RACDemo[47098:5449829] <RACTuple: 0x174016f50> (
1,
2
)
combineLatest
-
Combines multiple signals and takes the latest value from each. All combined signals must have called
sendNextat least once before the combined signal fires. -
Internals:
- When the combined signal is subscribed, signalA and signalB are subscribed internally. Both must have emitted values before the combination fires.
- The two signals' values are combined into a tuple and emitted.
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
// Combine the two signals — similar to zip in behavior
RACSignal *combineSignal = [signalA combineLatestWith:signalB];
[combineSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 13:36:07.461 RACDemo[47110:5450705] <RACTuple: 0x17401c240> (
1,
2
)
reduce
-
Aggregation: used when a signal emits tuples. Aggregates the tuple's values into a single value.
-
Common usage: combine then reduce.
combineLatest:(id<NSFastEnumeration>)signals reduce:(id (^)())reduceBlock -
About the
reduceBlock: thereduceBlockhas as many parameters as there are combined signals. Each parameter corresponds to the value emitted by its respective signal. ThereduceBlock's return value is the aggregated result. -
Internals: subscribing to the reduce signal causes
reduceBlockto be called each time content is emitted, converting the signal content to the value returned byreduceBlock.
RACSignal *signalA = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
return nil;
}];
RACSignal *signalB = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@2];
return nil;
}];
RACSignal *reduceSignal = [RACSignal combineLatest:@[signalA,signalB] reduce:^id(NSNumber *num1 ,NSNumber *num2){
return [NSString stringWithFormat:@"%@ %@",num1,num2];
}];
[reduceSignal subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 13:45:53.577 RACDemo[47143:5453271] 1 2
ReactiveCocoa Filter Operations
filter
-
Filters a signal; use it to receive only values that meet a condition.
-
Each time the signal emits, the filter condition is evaluated first.
-
The signal continues only when the block returns
YES.
[self.textField.rac_textSignal filter:^BOOL(NSString *value) {
return value.length > 3;
}];
ignore
-
Ignores signals whose value matches the specified value.
-
Internally calls
filterto exclude the ignored value.
[[self.textField.rac_textSignal ignore:@"1"] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
distinctUntilChanged
-
Emits a signal only when the current value is different from the previous value; otherwise the signal is ignored.
-
Filters out consecutive duplicate values.
-
Commonly used to avoid unnecessary UI refreshes — only refresh when the data actually changes.
[[self.textField.rac_textSignal distinctUntilChanged] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
take
- Takes only the first N signals from the beginning.
// 1. Create signal
RACSubject *signal = [RACSubject subject];
// 2. Process and subscribe to the signal
[[signal take:1] subscribeNext:^(id x) {
NSLog(@"%@", x);
}];
// 3. Send signals
[signal sendNext:@1];
[signal sendNext:@2];
2016-03-21 14:00:52.519 RACDemo[47218:5457876] 1
takeLast
- Takes only the last N signals. Requires the subscriber to call
sendCompletedfirst, since the total count is only known at completion.
// 1. Create signal
RACSubject *signal = [RACSubject subject];
// 2. Process and subscribe to the signal
[[signal takeLast:1] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
// 3. Send signals
[signal sendNext:@1];
[signal sendNext:@2];
[signal sendCompleted];
2016-03-21 14:02:57.279 RACDemo[47228:5458489] 2
takeUntil
- Receives values until a specified signal completes (e.g., monitor text field changes until the current object is deallocated).
[self.textField.rac_textSignal takeUntil:self.rac_willDeallocSignal];
takeWhileBlock
- Subscriber receives values only when the block logic returns
YES.
skip
- Skips the first N signals (e.g., the first emission is ignored).
[[self.textField.rac_textSignal skip:1] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
skipWhileBlock
Skips values while the block logic returns YES.
skipUntilBlock
Skips values until the block logic returns NO.
switchToLatest
-
Used with signal of signals. Sometimes a signal emits other signals.
switchToLatestretrieves the most recently emitted inner signal from a signal of signals. -
Gets the most recently emitted inner signal and subscribes to it. Note:
switchToLatestcan only be used with signal of signals.
RACSubject *signalOfSignals = [RACSubject subject];
RACSubject *signal = [RACSubject subject];
[signalOfSignals.switchToLatest subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
[signalOfSignals sendNext:signal];
[signal sendNext:@1];
ReactiveCocoa Order Operations
doNext
- Executes this block before executing
sendNext.
doCompleted
- Executes this block before executing
sendCompleted.
[[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendCompleted];
return nil;
}] doNext:^(id x) {
// Called before [subscriber sendNext:@1] is executed
NSLog(@"doNext");;
}] doCompleted:^{
// Called before [subscriber sendCompleted] is executed
NSLog(@"doCompleted");;
}] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 14:18:24.132 RACDemo[47282:5462606] doNext
2016-03-21 14:18:24.135 RACDemo[47282:5462606] 1
2016-03-21 14:18:24.136 RACDemo[47282:5462606] doCompleted
ReactiveCocoa Threading Operations
deliverOn
- Switches content delivery to the specified thread. Side effects remain on the original thread. Code in the signal creation block is considered a side effect.
subscribeOn
- Switches both content delivery and side effects to the specified thread.
ReactiveCocoa Timing Operations
timeout
- Timeout: causes a signal to automatically error after a specified duration.
RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
return nil;
}] timeout:1 onScheduler:[RACScheduler currentScheduler]];
[signal subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
// Automatically called after 1 second
NSLog(@"%@",error);
}];
2016-03-21 14:26:03.182 RACDemo[47310:5464263] Error Domain=RACSignalErrorDomain Code=1 "The operation couldn't be completed. (RACSignalErrorDomain error 1.)"
interval
- Timer: emits a signal at regular intervals.
[[RACSignal interval:1 onScheduler:[RACScheduler currentScheduler]] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 14:27:30.658 RACDemo[47323:5465171] 2016-03-21 06:27:30 +0000
2016-03-21 14:27:31.647 RACDemo[47323:5465171] 2016-03-21 06:27:31 +0000
2016-03-21 14:27:32.647 RACDemo[47323:5465171] 2016-03-21 06:27:32 +0000
...
delay
- Delays sending
next.
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
return nil;
}] delay:2] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
2016-03-21 14:28:54.966 RACDemo[47331:5465680] 1
ReactiveCocoa Repeat Operations
retry
- Retry: on failure, re-executes the signal creation block until it succeeds.
__block int i = 0;
[[[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
if (i == 10) {
[subscriber sendNext:@1];
} else {
NSLog(@"Error received");
[subscriber sendError:nil];
}
i++;
return nil;
}] retry] subscribeNext:^(id x) {
NSLog(@"%@",x);
} error:^(NSError *error) {
}];
2016-03-21 14:34:09.209 RACDemo[47358:5467295] Error received
2016-03-21 14:34:09.232 RACDemo[47358:5467295] Error received
2016-03-21 14:34:09.232 RACDemo[47358:5467295] Error received
2016-03-21 14:34:09.233 RACDemo[47358:5467295] Error received
2016-03-21 14:34:09.233 RACDemo[47358:5467295] Error received
2016-03-21 14:34:09.233 RACDemo[47358:5467295] 1
replay
- Replay: when a signal is subscribed multiple times, previously emitted content is replayed.
RACSignal *signal = [[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
[subscriber sendNext:@1];
[subscriber sendNext:@2];
return nil;
}] replay];
[signal subscribeNext:^(id x) {
NSLog(@"First subscriber: %@", x);
}];
[signal subscribeNext:^(id x) {
NSLog(@"Second subscriber: %@", x);
}];
2016-03-21 14:35:15.842 RACDemo[47367:5467753] First subscriber: 1
2016-03-21 14:35:15.843 RACDemo[47367:5467753] First subscriber: 2
2016-03-21 14:35:15.843 RACDemo[47367:5467753] Second subscriber: 1
2016-03-21 14:35:15.844 RACDemo[47367:5467753] Second subscriber: 2
throttle
-
Throttle: when a signal fires very frequently, throttle can be used to suppress emissions for a period of time, then emit only the latest value after that period ends.
-
Commonly used to optimize real-time search — prevents excessively frequent requests.
RACSubject *signal = [RACSubject subject];
self.signal = signal;
// Throttle: within the specified interval (0.3 seconds), ignore all signal emissions.
// After the interval, emit only the most recently sent signal value.
[[signal throttle:0.3] subscribeNext:^(id x) {
NSLog(@"%@",x);
}];
Code:
All code from this article can be found on my GitHub at RACDemo.