ReactiveCocoa Design Patterns Tweak a single line into justifying

Signals

April 5, 2014

Signals are essentially the building bricks of a program written in FRP. Basically, you program not what to do, but rather how many signals program has, where these signals take values from and how signals are connected with each other and thus how values passing through are transformed along the way. You can think of signal as a pipeline (@ashfurrow brings more on this analogy in his book) and your program is nothing but a complex pipeline system, when events are being passed as inputs to this pipeline, and results are presented when output comes out.

ReactiveCocoa signals

ReactiveCocoa 2.x brings some abstraction over signals concept, representing them as RACStream. Such streams can pass events of 3 types: value, error or complete. After sequence passes through error or complete event it is assumed to be completed and will never pass through new values (if any) pushed to it.

There are two types of streams: push-driven RACSignal and pull-driven RACSequence. Push-driven means that values for the signal are not defined at the moment of signal creation and may become available at a later time (for example, as a result from network request, or any user input). Pull-driven means that values in the sequence are defined at the moment of signal creation and we can query values from the stream one-by-one. In ReactiveCocoa sequences are usually created from Cocoa collections like NSArray, NSSet, NSDictionary or even NSIndexSet. In upcoming ReactiveCocoa version 3 however sequences are going to be deprecated, but this question is out of scope for this post.

RACSequence <=> RACSignal

RACSequence can be turned into RACSignal and vice versa. To turn RACSequence into RACSignal by scheduling it's sequence values into RACScheduler which will then push said values into newly created RACSignal:

    RACSequence *sequence = [@[@1, @2, @3] rac_sequence];
    RACSignal *signal = [sequence signalWithScheduler:[RACScheduler mainThreadScheduler]];

RACSignal can be turned into RACSequence by gathering it's values into some sort of buffer. There are several ways to do this. First is to gather all values sent to this RACSignal until that signal completes, then convert the buffer into RACSequence:

    RACSignal *signal = ...;
    NSArray *buffer = [signal toArray];
    RACSequence *sequence = [buffer rac_sequence];

One should note that -toArray method is blocking. It blocks until the signal completes, and that actually makes sense, we don't know how long and how many values will be available in the future.

The other way to turn RACSignal into RACSequence without waiting for signal to complete is to use -sequence method. This method creates private RACSignalSequence sequence which uses RACReplaySubject internally. Basically, this internal subject holds all values passed into this signal, and when accessing created sequence instance at a later time, it'll be sequence of all values received by the original signal until this time.

    RACSignal *signal = ...;
    RACSequence *sequence = [signal sequence];

Although -sequence method doesn't wait until original signal completes, it may block if that signal hasn't recevied any value yet. It will block until first value is passed into signal.

In ptractice, you mostly interested in RACSignal signals, as they represent future values, whereas RACSequence sequneces are mostly just a convenient way to apply sequence operation on a collections of objects.

Signal sources

The majority of signals existing in avarage program are derived signals. By derived I mean signals created by applying whatever operation to the other signal:

    RACSignal *existingSignal;
    RACSignal *newSignal = [[existingSignal map:^ id (id value) {
        return someAction(value);
    }] ignore: nil];

In this example newSignal is a signal pushing results of someAction(value) called on values pushed to originalSignal and any nil is ignored. However, there are two new signals created in this code snippet. First created by -map: method and then second — by calling -ignore: method on first newly created signal. Both of these methods create new signals.

But where is the original source of the signal? How one can push values to the signal? Actually, there are a lot of ways to create source of the signal values.

KVO observing

As seen in examples in ViewModel post, signals can be created as observer for KVO values:

    RACSignal *signal = RACObserve(object, propertyToObserve);

signal will be pushing new values of object.propertyToObserve as it changes. RACObserve is a macro expanded to -rac_valuesForKeyPath:observer:. Hidden caveat here: this macro always captures self as an observer, so if you use this macro inside a block, make sure you're not casuing retain cycle.

UIKit Categories

ReactiveCocoa provides a set of categories for UIKit classes to generate signals for user-generated events. It's very important as we all know, UIKit succs when it comes to KVO observing.

RACSubject

RACSubject is literally a bridge between reactive and non-reactive code. One can manually push values into subject and subjects are RACSignals too (in fact, RACSubject is a subclass of RACSignal), so they can be subscribed to.

    RACSubject *subject = [RACSubject subject];
    RACSignal *derived = [subject map:^(id value) {
        return someAction(value);
    }];
    [subject sendNext:@YES]; // at this point value @YES is pushed into 'derived' signal


Dynamic signals

The most interesting are dynamic signals backed by private RACDynamicSignal class. It's called 'dynamic' because it's user created. You can turn any asynchronus action into a signal, here's a typical pattern:

    RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
        id operation = asynchronusAction(^(id asyncActionResult, NSError *error) {
            if (error) {
                [subscriber sendError:error];
            }
            else {
                [subscriber sendNext:asyncActionResult];
                [subscriber sendCompleted];
            }
        });
        return [RACDisposable disposableWithBlock:^{
            [operation cancel];
        }];
    }];

where asynchronusAction() takes a block as a callback.

Don't be scared by this 'disposable' thing, we'll get to it back later. In short: if asynchronous operation can be cancelled, we want to cancel it when signal is destroyed (in ReactiveCocoa's terms — disposed).

Subscription

We first see subscriber in previous example. That's the basics of singals infrastructure in ReactiveCocoa — subscription. Signals are connected together through subscribing. Let's pretend we want to create another signal from previous one by mapping values:

    RACSignal *mapSignal = [signal map:^id (id value) {
        return someAction(value);
    }];

Here's what will happen: when second signal subscribes to first one, that block we passed in '-createSignal:' is called and subscriber value passed into it is our second signal (yes, RACSignal conforms to RACSubscriber protocol). Note that asynchronusAction() is not called until someone subscribes to dynamic signal. Actions defined in subscription block only occur upon subscription. You can think of it the other way: dynamic signal we created is turned on when someone subscribes to it.

Another thing to notice: subscription block will be called for each subscriber. So, if create another derived signal:

    RACSignal *anotherMapSignal = [signal map:^id (id value) {
        return anotherAction(value);
    }];

there will be another subscription to original dynamic signal and subscription block will be called again!. In this particular scenario, we most likely want asycnrhonous action to be performed once and multiple subscribers will mess things up. To workaround this issue there is RACConnection concept. Long story short, it shares underlying signal with subscribers through internal RACSubject. We'll see how it works later.

To be strict, in those examples I gave actually, no subscription ever happen. That's because -map: internally creates another dynamic signal and subscribes to original signal only upon it's own subscription. That means subscriptions happen in reverse order over the chain of signals. To trigger subscriptions the last signal in chain must be subscribed to 'real' subscriber, e.g. who subscribes immediately:

    [anotherMapSignal subscribeNext:^(id value) {
        //whatever
    }];

or

    RAC(object, property) = anotherMapSignal;
Vote on Hacker News