Published on

Introduction to ReactiveCocoa

Overview of ReactiveCocoa

ReactiveCocoa is inspired by functional reactive programming. Rather than using mutable variables which are replaced and modified in-place, RAC offers "event streams," represented by the Signal and SignalProducer types, that send values over time.

ReactiveCocoa is inspired by Functional Reactive Programming (FRP). Instead of using mutable variables that change in place, RAC captures value changes through event streams, represented by Signal and SignalProducer.

Event streams unify all of Cocoa's common patterns for asynchrony and event handling, including:

  • Delegate methods
  • Callback blocks
  • NSNotifications
  • Control actions and responder chain events
  • Futures and promises
  • Key-value observing (KVO)

In iOS development, we frequently respond to events to handle business logic — button taps, pull-to-refresh, network requests, property changes (via KVO), or user location updates (via CoreLocation). However, all these events are handled in different ways: actions, delegates, KVO, callbacks, etc.

In fact, all of these events can be handled through RAC. ReactiveCocoa provides many methods for handling events, and using RAC makes event handling very convenient. You can place the handling code and the listening code in the same place, making management much easier without needing to jump to separate methods. This aligns well with the high cohesion, low coupling principle in software development.

Common ReactiveCocoa Classes

RACSignal

The signal class generally represents data that will be delivered in the future. Whenever data changes, the signal receives it internally and emits it immediately. Note: it's the data that is emitted, not the signal class itself.

  • A signal class (RACSignal) only indicates that data will be sent when it changes. The signal itself does not have the ability to send; it delegates that to an internal subscriber.

  • By default, a signal is a cold signal — it won't trigger even if the value changes. Only after a subscription is made does it become a hot signal and start triggering on value changes.

  • How to subscribe to a signal: call subscribeNext on the RACSignal.

Steps for using RACSignal:

  1. Create a signal: + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
  2. Send a signal: - (void)sendNext:(id)value
  3. Subscribe to the signal to activate it: - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock

RACSignal internals:

  1. A subclass signal RACDynamicSignal is created. First, the didSubscribe block is saved in RACDynamicSignal, but it is not yet triggered (cold signal).

  2. When the signal is subscribed — i.e., subscribeNext:nextBlock is called on the signal — subscribeNext internally creates a subscriber and saves the nextBlock into it. At this point it becomes a hot signal.

  3. subscribeNext internally calls the didSubscribe block of RACDynamicSignal. Typically, [subscriber sendNext:@1] is called inside didSubscribe.

  4. sendNext under the hood executes the subscriber's nextBlock.

  5. Mark the signal as completed or unsubscribe.

  6. Execute the code in RACDisposable's disposeBlock.

Code:

// 1.Create signal createSignal:didSubscribe(block)
// RACDisposable: cancel subscription
// RACSubscriber: send data
RACSignal *siganl = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {

   // block call timing: called every time a subscriber subscribes to the signal.
   // block purpose: describes what data the signal needs to send
   // _subscriber = subscriber;

   // 3.Send signal
   NSLog(@"didSubscribe called");
   // Typically: pass data out
   [subscriber sendNext:@1];
   // This calls the subscriber's nextBlock

   // 5.If no more data needs to be sent, it's best to send a completion signal.
   // Internally this will automatically call [RACDisposable disposable] to cancel the subscription.
   // Or if you want to cancel the signal, return a RACDisposable and later call [disposable dispose].
   [subscriber sendCompleted];

   return [RACDisposable disposableWithBlock:^{
       // 6.When is a signal cancelled: 1. Auto-cancel, when a signal's subscriber is destroyed 2. Manual cancel
       // block call timing: called once a signal is unsubscribed
       // block purpose: clean up resources when the signal is unsubscribed
       NSLog(@"Unsubscribed");
   }];
}];

// subscribeNext: creates a subscriber and saves the nextBlock into it
// 2.Subscribe to the signal: subscribing returns an object for cancelling the subscription
[siganl subscribeNext:^(id x) {
   // 4.block call timing: called every time the signal emits data.
   NSLog(@"Received data: %@", x);
}];

// 5.Cancel subscription
//    [disposable dispose];
2016-03-19 21:51:02.587 RACDemo[44522:5148595] didSubscribe called
2016-03-19 21:51:02.588 RACDemo[44522:5148595] Received data: 1
2016-03-19 21:51:02.589 RACDemo[44522:5148595] Unsubscribed

RACSubscriber

Represents a subscriber. It is used to send signals. This is a protocol, not a class — any object that conforms to this protocol and implements its methods can act as a subscriber. Signals created via createSignal all have a subscriber that helps them send data.

RACDisposable

Used to cancel subscriptions or clean up resources. It is triggered automatically when a signal completes or sends an error.

RACSubject

A signal provider that can both act as a signal and send signals.

Use case: commonly used to replace delegates. With RACSubject, you no longer need to define a delegate.

Steps for using RACSubject

  1. Create a signal: [RACSubject subject] — unlike RACSignal, there is no block when creating the signal.

  2. Subscribe to the signal: - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock.

  3. Send a signal: sendNext:(id)value.

RACSubject internals differ from RACSignal:

  1. Calling subscribeNext to subscribe only saves the subscriber. The subscriber's nextBlock is already set.

  2. Calling sendNext to send a signal iterates over all saved subscribers and calls each one's nextBlock.

Code:

// RACSubject: signal provider

// 1.Create signal
RACSubject *subject = [RACSubject subject];

// 2.Subscribe to signal
[subject subscribeNext:^(id x) {

   // 4.block: called when data is emitted
   // block: handle the data
   NSLog(@"First subscriber: %@", x);
}];

// Send signal
//    [subject sendNext:@1];

// 2.1 Second subscription
[subject subscribeNext:^(id x) {
   // block call timing: called when the signal emits a new value.
   NSLog(@"Second subscriber: %@", x);
}];

// 3.Send signal
[subject sendNext:@1];
[subject sendNext:@2];
2016-03-19 22:00:09.573 RACDemo[44558:5150846] First subscriber: 1
2016-03-19 22:00:09.574 RACDemo[44558:5150846] Second subscriber: 1
2016-03-19 22:00:09.575 RACDemo[44558:5150846] First subscriber: 2
2016-03-19 22:00:09.575 RACDemo[44558:5150846] Second subscriber: 2

Using RACSubject to replace a delegate:

//In SubjectSecondViewController.h, add a RACSubject property to replace the delegate.
@property (nonatomic, strong) RACSubject *subject;

//Listen for the button click in SubjectSecondViewController
- (IBAction)backDidClick:(UIButton *)sender {

    // 4.When the back button is tapped, notify SubjectViewController to do something
    if (self.subject) {

        [self.subject sendNext:@1];
    }
}

// In SubjectViewController, listen for the navigation button, assign the delegate signal for SubjectSecondViewController, and subscribe to it
- (IBAction)buttonDidClick:(UIButton *)button {

    UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];

    SubjectSecondViewController *twoVc = [storyboard instantiateViewControllerWithIdentifier:@"two"];

    //1.Create and set the signal provider
    twoVc.subject = [RACSubject subject];

    // 2.Subscribe to the signal
    @weakify(twoVc)
    [twoVc.subject subscribeNext:^(id x) {

        NSLog(@"Notified ViewController: %@", x);
        @strongify(twoVc)
        [twoVc dismissViewControllerAnimated:YES completion:^{

        }];
    }];

    // 3.Present the controller
    [self presentViewController:twoVc animated:YES completion:nil];

}

RACReplaySubject

A replay signal provider; a subclass of RACSubject.

Use case 1: If a signal needs to re-send all previously emitted values each time it is subscribed to, use the replay signal class.

Use case 2: You can set a capacity to limit the number of cached values — only the most recent N values are cached.

  1. Create a signal: [RACReplaySubject subject] — unlike RACSignal, there is no block when creating the signal.
  2. You can subscribe before or after sending.
    1. Subscribe: - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
    2. Send: sendNext:(id)value

RACReplaySubject internals differ from RACSubject:

  1. Calling sendNext saves the value and then iterates over all saved subscribers, calling each one's nextBlock.
  2. Calling subscribeNext iterates over all saved values and calls the subscriber's nextBlock for each one.

If you want a signal to replay all previous values when subscribed, you need to send before subscribing — in other words, save the values first, then subscribe.

Code:

// 1.Create signal
RACReplaySubject *subject = [RACReplaySubject subject];

// 2.Subscribe to signal
[subject subscribeNext:^(id x) {

   NSLog(@"First subscriber: %@", x);
}];

// 3.Send signal
[subject sendNext:@1];

[subject sendNext:@2];

[subject subscribeNext:^(id x) {

   NSLog(@"Second subscriber: %@", x);
}];
2016-03-19 22:02:33.387 RACDemo[44558:5150846] First subscriber: 1
2016-03-19 22:02:33.387 RACDemo[44558:5150846] First subscriber: 2
2016-03-19 22:02:33.389 RACDemo[44558:5150846] Second subscriber: 1
2016-03-19 22:02:33.390 RACDemo[44558:5150846] Second subscriber: 2

From the output, you can see that regardless of whether sendNext comes before or after the subscription, the output is the same. With RACSubject, the same code order would produce:

2016-03-19 22:04:41.971 RACDemo[44572:5151777] First subscriber: 1
2016-03-19 22:04:41.972 RACDemo[44572:5151777] First subscriber: 2

That is, the subscriber added after sendNext receives nothing.

RACTuple

A tuple class, similar to NSArray, used to wrap values.

The RACTupleUnpack macro is specifically used to unpack tuples. The right side of the equals sign is the tuple to unpack. The macro's parameters specify the types to unpack into. The number of macro parameters must match the number of values in the tuple.

// Iterate over a dictionary; each key-value pair is wrapped into a RACTuple
NSDictionary *dict = @{@"name":@"Tony", @"age":@18};
[dict.rac_sequence.signal subscribeNext:^(RACTuple *x) {
   // Unpack the tuple, assigning values in order to the parameters
   RACTupleUnpack(NSString *key, NSString *value) = x;
   // Equivalent to:
   // NSString *key = x[0];
   // NSString *value = x[1];
   NSLog(@"%@ %@", key, value);
}];
2016-03-19 22:38:18.100 RACDemo[44687:5159036] name Tony
2016-03-19 22:38:18.105 RACDemo[44687:5159036] age 18

RACSequence

A collection class in RAC, used to replace NSArray and NSDictionary. It can be used to quickly iterate over arrays and dictionaries, and is useful for mapping dictionaries to model objects.

// Iterate over an array
NSArray *arr = @[@1,@2,@3];

// This is actually three steps:

// Step 1: Convert the array to a RACSequence: arr.rac_sequence
// Step 2: Convert the RACSequence to a RACSignal: arr.rac_sequence.signal
// Step 3: Subscribe to the signal — activating it will automatically iterate over all values in the collection.
[arr.rac_sequence.signal subscribeNext:^(id x) {
   NSLog(@"%@",x);
}];
2016-03-19 22:34:23.459 RACDemo[44674:5158285] 1
2016-03-19 22:34:23.460 RACDemo[44674:5158285] 2
2016-03-19 22:34:23.461 RACDemo[44674:5158285] 3

Dictionary to model mapping:

NSString *filePath = [[NSBundle mainBundle] pathForResource:@"flags.plist" ofType:nil];
NSArray *dictArr = [NSArray arrayWithContentsOfFile:filePath];

//OC (traditional approach)
NSMutableArray *items = [NSMutableArray array];
for (NSDictionary *dict in dictArr) {
   FlagItem *item = [FlagItem flagWithDict:dict];
   [items addObject:item];
}

//RAC approach
NSMutableArray *flags = [NSMutableArray array];
// Note: calling subscribeNext on rac_sequence does not execute nextBlock immediately; it waits briefly.
[dictArr.rac_sequence.signal subscribeNext:^(id x) {
   // Using RAC to iterate a dictionary; x is the dictionary
   FlagItem *item = [FlagItem flagWithDict:x];
   [flags addObject:item];
}];

// Using map
// map: maps the original value to a new value
// array: converts the collection to an array
// Internals: when the signal is subscribed, it iterates over original values in the collection, maps them to new values, and saves them to a new array.
NSArray *newFlags = [[dictArr.rac_sequence map:^id(id value) {
   return [FlagItem flagWithDict:value];
}] array];

NSLog(@"%@, %@, %@", items, flags, newFlags);

RACCommand

A class in RAC used to handle events. It wraps event handling logic and data passing into a single object, and makes it easy to monitor the execution state of an event.

Use cases: monitoring button taps, network requests.

  • RACCommand usage steps:
  1. Create a command: initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
  2. Inside the signalBlock, create a RACSignal and return it as the signalBlock's return value.
  3. Execute the command: - (RACSignal *)execute:(id)input
  • RACCommand usage notes:
  1. signalBlock must return a signal; it cannot return nil.
  2. If you don't want to pass any signal data, create an empty signal: [RACSignal empty].
  3. The signal inside RACCommand must call [subscriber sendCompleted] when data transmission is complete, otherwise the command will remain in the "executing" state indefinitely.
  4. RACCommand must be strongly referenced; otherwise, you won't receive signals from within it, because RACCommand's signals are sent lazily.
  • RACCommand design rationale: why does the internal signalBlock return a signal, and what is that signal used for?
  1. In RAC development, network requests are typically wrapped inside a RACCommand. Executing a RACCommand sends the request.
  2. When RACCommand receives data from the request, it needs to pass that data to the outside world — which is done via the signal returned by signalBlock.
  • How to get data from the signal returned by RACCommand:
  1. RACCommand has an executionSignals execution signal source. This is a signal of signals — the data emitted by this signal is itself a signal, not a plain value.
  2. Subscribing to executionSignals gives you the signal returned by RACCommand. Then subscribing to the signalBlock's returned signal gives you the actual emitted values.
  • Monitor whether the current command is executing via executing.
// Create command
RACCommand *command = [[RACCommand alloc] initWithSignalBlock:^RACSignal *(id input) {

   // block call timing: called when this command is executed
   NSLog(@"Executing command: %@", input);
   // block purpose: describes how to handle the event, e.g., a network request

   // Create an empty signal — must return a signal
   // return [RACSignal empty];

   // 2.RACCommand must return a signal. When handling an event, data is produced and sent through this returned signal.
   // Note: when all data has been sent, call sendCompleted so the command finishes.
   return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
       // block purpose: send the event-handling signal
       // block call timing: called when the signal is subscribed to
       [subscriber sendNext:@"Signal content"];
       [subscriber sendCompleted];

       return nil;
   }];
}];

// Strongly reference the command so it isn't destroyed and we can receive data
_command = command;
// executionSignals: the signal source containing all event-handling signals.
// executionSignals: signalOfSignals — the signal emits signals, not plain values

// 3.To receive signals from the execution signal source, make sure the command is not destroyed
[command.executionSignals subscribeNext:^(id x) {
   // x -> signal
   [x subscribeNext:^(id x) {

       NSLog(@"%@",x);
   }];
}];
// 4.Execute the command, invoking signalBlock
[command execute:@1];

// Advanced RAC usage
// switchToLatest: used for signal of signals; gets the latest signal emitted by a signal of signals,
// so you can directly receive the signal inside RACCommand
[command.executionSignals.switchToLatest subscribeNext:^(id x) {
   NSLog(@"%@",x);
}];

// 5.Monitor whether the command has finished executing.
// It fires once by default, which can be skipped. skip: skips the first signal.
[[command.executing skip:1] subscribeNext:^(id x) {
   if ([x boolValue] == YES) {
       // Currently executing
       NSLog(@"Executing");

   }else{
       // Finished executing
       NSLog(@"Finished");
   }
}];

RACMulticastConnection

Used when a signal is subscribed to multiple times. To avoid triggering the signal creation block multiple times and causing side effects, use this class.

Usage note: RACMulticastConnection is created via RACSignal's - (RACMulticastConnection *)publish or - (RACMulticastConnection *)multicast:(RACSubject *)subject method.

  • RACMulticastConnection usage steps:
  1. Create a signal: + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
  2. Create a connection: RACMulticastConnection *connect = [signal publish];
  3. Subscribe to the signal. Note: you subscribe to the connection's signal, not the original signal. [connect.signal subscribeNext:nextBlock]
  4. Connect: [connect connect]
  • RACMulticastConnection internals:
  1. Creating connect gives: connect.sourceSignal -> RACSignal (original signal), connect.signal -> RACSubject.
  2. Subscribing to connect.signal calls RACSubject's subscribeNext, creating and saving a subscriber without executing the block.
  3. [connect connect] internally subscribes to the original RACSignal, with RACSubject as the subscriber.
    1. Subscribing to the original signal invokes its didSubscribe.
    2. Inside didSubscribe, calling sendNext on the subscriber actually calls RACSubject's sendNext.
  4. RACSubject's sendNext iterates over all of RACSubject's subscribers and sends the signal.
    1. Since step 2 involved subscribing to RACSubject, all subscribers from step 2 are retrieved and their nextBlock is called.

Problem: Suppose a signal wraps a network request. Every subscription triggers a new request, leading to multiple requests. Solution: Use RACMulticastConnection to fix this.

// Send a request wrapped in a signal — regardless of how many subscribers, only want to send once
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
   // Code inside didSubscribe is collectively called "side effects."
   // Send request
   NSLog(@"Sending request");
   [subscriber sendNext:@1];
   return nil;
}];
// Subscribe to signal
[signal subscribeNext:^(id x) {
   NSLog(@"Received data: %@", x);
}];
[signal subscribeNext:^(id x) {
   NSLog(@"Received data: %@", x);
}];
2016-03-20 13:02:59.130 RACDemo[45388:5236769] Sending request
2016-03-20 13:02:59.134 RACDemo[45388:5236769] Received data: 1
2016-03-20 13:02:59.134 RACDemo[45388:5236769] Sending request
2016-03-20 13:02:59.135 RACDemo[45388:5236769] Received data: 1

The output shows the request is sent twice — once per subscription.

// RACMulticastConnection: solves the duplicate request problem
// 1.Create signal
RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
   NSLog(@"Sending request");
   [subscriber sendNext:@1];
   return nil;
}];
// 2.Create connection
RACMulticastConnection *connect = [signal publish];
// 3.Subscribe to the signal.
// Note: subscribing does not activate the signal; it only saves subscribers into an array.
// Activation happens via connect, which calls sendNext: on all subscribers at once.
[connect.signal subscribeNext:^(id x) {
   NSLog(@"Subscriber one signal");
}];
[connect.signal subscribeNext:^(id x) {
   NSLog(@"Subscriber two signal");
}];
// 4.Connect, activate the signal
[connect connect];
2016-03-20 13:05:13.803 RACDemo[45398:5237395] Sending request
2016-03-20 13:05:13.804 RACDemo[45398:5237395] Subscriber one signal
2016-03-20 13:05:13.804 RACDemo[45398:5237395] Subscriber two signal

RACScheduler

A queue in RAC, implemented using GCD.

RACUnit

Represents a stream that contains no meaningful value. When you see this, you can think of it as nil.

RACEvent

Wraps data into a signal event (signal event). It is primarily used via RACSignal's -materialize method.

References:

Framework Overview

Getting Started with ReactiveCocoa — The Basics

Code:

All code from this article can be found on my GitHub at RACDemo.