Article summary
One of the many useful things I’ve found in ReactiveCocoa (a functional reactive programming library for iOS) is the way that it can abstract away the asynchronous callback nature of some iOS core frameworks. And, by making use of RACDisposables, it’s easy to take care of cleanup work like closing connections or stopping a service.
There are a few key ReactiveCocoa concepts I’ve found myself using repeatedly when writing a RAC interface to an iOS library. In this post, I’m going to discuss those concepts and use them in an example, to build up a made-up service that asynchronously provides score updates to college basketball games.
An Example Service
The service I’ll use in this example requires that a delegate object be provided to the library. A -start
method needs to be called when updates are desired. While the service is running, a callback method will be called whenever there is a scoring change. When updates are no longer desired, a -stop
method can be called.
ReactiveCocoa Concepts
In order to wrap an asynchronous callback-driven iOS library, you will need to have a handle on the following ReactiveCocoa concepts/features:
- Side Effects
- Signal for Selector
- Laziness
- Merging Signals
- Cleanup with Disposables
Side Effects
You will often read that, when using ReactiveCocoa or doing functional reactive programming, side effects should be avoided.
The primary use of ReactiveCocoa, in an iOS app, is usually to bind data to a user interface element. Data can often go through a number of transformations as it moves from its source to its final destination. For example, some raw binary data might be unpacked, run through a formula, rounded off, and displayed as a percentage to the user. The RACSignal that is bound to the UILabel, in this case, should not have any side effects—this is where the functional part of Functional Reactive Programming comes in.
But in addition to binding data to the UI, RACSignals can be a great way to manage stateful interactions with iOS frameworks and libraries.
To do this properly, it is very important to understand that with a RACSignal, side effects occur for each subscription:
Each new subscription to a RACSignal will trigger its side effects. This means that any side effects will happen as many times as subscriptions to the signal itself.
As mentioned in the linked documentation, there are ways to suppress that behavior, so it’s just something you need to keep in mind.
It’s also recommended that you make the side effects of a signal explicit when possible:
the use of -doNext:, -doError:, and -doCompleted: will make side effects more explicit and self-documenting
To follow this guideline, I try to put all of my calls to external libraries into one of the do
operators. Sometimes that doesn’t make sense though, like in the case of a signal that does nothing besides call an external library. In that case, I’ll often use the +defer
operator or +createSignal:
to wrap the call with side-effects.
Signal for Selector
Callback-driven iOS frameworks will call methods on a delegate object to provide asynchronous updates. A ReactiveCocoa interface to such a framework will require getting the data from those callbacks into a signal.
The proper way to do that is with the use of the rac_signalForSelector: or rac_signalForSelector:fromProtocol: operators. From the header documentation for the rac_signalForSelector:
operator:
Returns a signal which will send a tuple of arguments upon each invocation of the selector, then completes when the receiver is deallocated. This is useful for changing an event or delegate callback into a signal. For example, on an NSView:
[[view rac_signalForSelector:@selector(mouseDown:)]
subscribeNext:^(RACTuple *args) {
NSEvent *event = args.first;
NSLog(@"mouse button pressed: %@", event);
}];
In my example with the college basketball score service, I can create a signal of all of the score updates coming from the service as follows:
RACSignal *incomingScoresSignal = [[self rac_signalForSelector:@selector(scoreUpdated:timeRemaining:)]
reduceEach:^id(NSString *score, NSNumber *timeRemaining) {
return [NSString stringWithFormat:@"%@ with %@ seconds to go", score, timeRemaining];
}];
-rac_signalForSelector
yields a RACTuple
each time the callback is called. Using the -reduceEach:
operator is a convenient way to map from a tuple to something else in your application’s domain. It’s basically the same as doing a -map:
but you can unpack the tuple right in the passed block’s parameters, as opposed to just taking a single RACTuple
argument like a -map:
would.
Laziness
Wikipedia defines lazy evaluation as follows:
In programming language theory, lazy evaluation, or call-by-need is an evaluation strategy which delays the evaluation of an expression until its value is needed (non-strict evaluation)…
When using ReactiveCocoa to manage side-effects, I’ve found it’s best to be lazy about everything. If I call a method that returns a RACSignal, and that signal is wrapping one or more side-effects, I expect that calling the method will do nothing more than create the signal and return it. In other words, I expect that no side effects will occur until the returned signal is subscribed to.
When I first started working with ReactiveCocoa, I would find myself writing methods like this:
- (RACSignal *)calculateTomorrowsDate {
NSDate *tomorrow = [[NSDate date] dateByAddingTimeInterval:24 * 60 * 60];
return [RACSignal return:tomorrow];
}
With this implementation the current date is retrieved immediately, the calculation is done immediately, and a signal is created that returns the calculated result. The value sent on the signal will be based on when the method was called, not when the signal is subscribed to.
This (what I believe to be) incorrect way of wrapping behavior in a RACSignal seems straightforward at first, but it can lead to unexpected behavior when using this signal with operators like +retry
.
Instead, you should evaluate everything lazily:
- (RACSignal *)calculateTomorrowsDate {
return [RACSignal defer:^{
NSDate *tomorrow = [[NSDate date] dateByAddingTimeInterval:24 * 60 * 60];
return [RACSignal return:tomorrow];
}];
}
When calling this version of the method, a new signal is created and returned – but the actual work is lazily deferred until something subscribes to the resulting signal. If subscription doesn’t happen right away it’s not a problem because the [NSDate date]
call won’t happen until the signal is subscribed to. And if the signal is subscribed to a second or a third time, also not a problem. Each subscriber will result in the deferred code being executed again so the date will be correct each time.
This becomes especially important when writing a method that does nothing more than create a side-effect. For example, a method to start the basketball score service would look like:
- (RACSignal *)startScoreService {
@weakify(self)@
return [RACSignal defer:^RACSignal * {
@strongify(self)
[self.scoreService start];
return [RACSignal empty];
}];
}
By lazily starting the service only when the signal is subscribed to, this signal can now be incorporated into a more complicated signal that waits for another signal to complete, or re-subscribes if there is an error, etc.
Merging Signals
In ReactiveCocoa, it is quite common to merge multiple signals to provide a single stream of values, as described in the Merging section of the Basic Operators documentation.
When working in the land of side-effects, I’ve found merging to be an important tool as well. In this case, I’m rarely interested in multiple signals that send values being combined into a single stream of values. More often, I’m looking to combine the behavior of multiple signals. Usually only one of the merged signals will send any values at all (often something coming in from a rac_signalForSelector
callback). The others just do something when they are subscribed to.
Once the score service is started, it invokes a callback on its delegate each time a score changes. In order to avoid missing any callbacks, I’m going to want to subscribe to the callback signal first, then start the service.
- (RACSignal *)retrieveScoreUpdates {
@weakify(self)
RACSignal *startServiceSignal = [RACSignal defer:^RACSignal * {
@strongify(self)
[self.scoreService start];
return [RACSignal empty];
}];
RACSignal *incomingScoresSignal =
[[self rac_signalForSelector:@selector(scoreUpdated:timeRemaining:)]
reduceEach:^id(NSString *score, NSNumber *timeRemaining) {
return [NSString stringWithFormat:@"%@ with %@ seconds to go", score, timeRemaining];
}];
return [RACSignal merge:@[incomingScoresSignal, startServiceSignal]];
}
The +merge
will first subscribe to the callback signal, making sure we’re listening for events before telling the service to start sending events. The signal that tells the service to start doesn’t send any values. It just starts the service, and completes immediately.
Cleanup with Disposables
According to the Design Guidelines, in ReactiveCocoa disposal cancels in-progress work and cleans up resources. This ability can really come in handy when working with a service that needs to be started and stopped, or with a connection that needs to be closed when done, etc.
Here’s an example of a signal that does nothing but stop the score service when the signal is disposed of:
@weakfify(self)
RACSignal *stopServiceSignal =
[RACSignal createSignal:^RACDisposable *(id subscriber) {
return [RACDisposable disposableWithBlock:^{
@strongify(self)
[self.scoreService stop];
// If nothing references the subscriber the signal will be disposed
// of immediately - reference it here just to prevent that.
subscriber;
}];
}];
If you want to be sure to stop the service when you are done listening to its callbacks, you can merge this signal into the signal that is listening to the callbacks. When you dispose of that merged signal the service will automatically be stopped.
Put it All Together
Putting all of these concepts together, I can write a method that will return a signal that, when subscribed to, will start the score service; will yield a string that describes the latest score, each time the score changes; and stops the service whenever the signal is disposed of. For the purposes of this example, I’m going to assume that the scoreService
property has already been instantiated (likely in the initializer) and self
has been set as the delegate object of the service.
- (RACSignal *)retrieveScoreUpdates {
@weakify(self)
RACSignal *startServiceSignal = [RACSignal defer:^RACSignal * {
@strongify(self)
[self.scoreService start];
return [RACSignal empty];
}];
RACSignal *stopServiceSignal =
[RACSignal createSignal:^RACDisposable *(id subscriber) {
return [RACDisposable disposableWithBlock:^{
@strongify(self)
[self.scoreService stop];
// If nothing references the subscriber the signal will be disposed
// of immediately - reference it here just to prevent that.
subscriber;
}];
}];
RACSignal *incomingScoresSignal =
[[self rac_signalForSelector:@selector(scoreUpdated:timeRemaining:)]
reduceEach:^id(NSString *score, NSNumber *timeRemaining) {
return [NSString stringWithFormat:@"%@ with %@ seconds to go", score, timeRemaining];
}];
return [RACSignal merge:@[
incomingScoresSignal,
startServiceSignal,
stopServiceSignal
]];
}
A consumer of this signal might want to grab the next 5 score updates and provide them on the main thread. After those 5 score updates, the signal will be disposed of and the score service will be stopped. The following code might be in a ViewModel object.
_scoreUpdateSignal =
[[[helper retrieveScoreUpdates] take:5] deliverOnMainThread];
And thanks to the power of lazy-evaluation, the score service won’t even be started until something subscribes to this signal!
Conclusion
I’ve found ReactiveCocoa to be a fantastic way to abstract away the complexities of asynchronous callback-driven frameworks and libraries. By getting a handle on the concepts I’ve discussed in this post, I hope that more people can find more ways to incorporate this fantastic library into their apps.
Awesome post, thanks! I didn’t know about [RACSignal defer:], and it seems to be really useful.