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.

  • bind parameter: takes a block that returns a RACStreamBindBlock.

  • RACStreamBindBlock is 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 importing RACReturnSignal.h.

Steps for using bind:

  1. Pass in a block that returns RACStreamBindBlock.

  2. Describe a bindBlock of type RACStreamBindBlock as the block's return value.

  3. Describe a result signal as the bindBlock's return value.

Note: handle signal result processing inside bindBlock.

Internals:

  1. Calling bind on the source signal creates a new bound signal.

  2. When the bound signal is subscribed to, its didSubscribe is called, generating a bindingBlock.

  3. When the source signal emits a value, it is passed to bindingBlock for processing via bindingBlock(value, stop).

  4. bindingBlock(value, stop) returns a signal (RACReturnSignal) with the processed content.

  5. Subscribing to RACReturnSignal retrieves 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 use RACReturnSignal.

[[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

  • flattenMap and Map are 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:

  1. Pass in a block with return type RACStream and parameter value.

  2. value is the source signal's content. Use it to do your processing.

  3. Wrap the result into a RACReturnSignal and return it.

flattenMap internals:

  1. flattenMap is implemented using bind internally. The block's return value in flattenMap becomes the return value of bindBlock in bind.

  2. When the bound signal is subscribed to, bindBlock is generated.

  3. When the source signal sends content, bindBlock(value, *stop) is called.

  4. bindBlock internally calls flattenMap's block, which wraps the processed data into a signal.

  5. The returned signal ultimately serves as the return signal of bindBlock.

  6. 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
  • Map usage: 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:

  1. Pass in a block that returns an object and takes a value parameter.
  2. value is the source signal's content — use it directly.
  3. Return the processed content directly. No need to wrap it in a signal.
  • Map internals:
  1. Map internally calls flattenMap. The return value from Map's block becomes the value in flattenMap's block.
  2. When the bound signal is subscribed to, bindBlock is generated.
  3. When the source signal sends content, bindBlock(value, *stop) is called.
  4. bindBlock internally calls flattenMap's block.
  5. flattenMap's block calls Map's block and wraps its return value into a signal.
  6. The returned signal ultimately serves as the return signal of bindBlock.
  7. 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

  1. The block in FlatternMap returns a signal.
  2. The block in Map returns an object.
  3. In practice, if the signal's emitted value is not itself a signal, use Map.
  4. 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.

  • concat internals:

  1. When the concatenated signal is subscribed, its didSubscribe is called.
  2. didSubscribe first subscribes to the first source signal (signalA).
  3. signalA's didSubscribe is executed.
  4. When signalA's didSubscribe sends values, signalA's subscriber's nextBlock is called and the values are forwarded via the concatenated signal's subscriber.
  5. When signalA's didSubscribe sends a completion, signalA's subscriber's completedBlock is called and signalB is subscribed, activating it.
  6. signalB's didSubscribe is executed.
  7. When signalB's didSubscribe sends 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:

  1. First filters out values emitted by the previous signal.
  2. Then uses concat to connect the signal returned by then.
[[[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:

  1. When the merged signal is subscribed, it iterates over all signals and subscribes to each.
  2. Each signal is subscribed as it is enumerated.
  3. As soon as the merged signal is subscribed, all internal signals are subscribed.
  4. 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:

  1. Defining the zip signal automatically subscribes to signalA and signalB internally.
  2. 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 sendNext at least once before the combined signal fires.

  • Internals:

  1. When the combined signal is subscribed, signalA and signalB are subscribed internally. Both must have emitted values before the combination fires.
  2. 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: the reduceBlock has as many parameters as there are combined signals. Each parameter corresponds to the value emitted by its respective signal. The reduceBlock's return value is the aggregated result.

  • Internals: subscribing to the reduce signal causes reduceBlock to be called each time content is emitted, converting the signal content to the value returned by reduceBlock.

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 filter to 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 sendCompleted first, 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. switchToLatest retrieves the most recently emitted inner signal from a signal of signals.

  • Gets the most recently emitted inner signal and subscribes to it. Note: switchToLatest can 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.