Centralize Callback Handling by Creating a Reactive Signal

Article summary

When I first started using ReactiveCocoa (now ReactiveObjC), I mainly stuck to the basic operators: map, flatten, merge, etc. I saw a few examples of createSignal, but all I saw was a lot of manual work with disposables, subscribers, and other tedious sorts of things that I didn’t really want to mess with.

But one of the things that reactive programming excels at is pulling all of the parts needed for an operation into one place. The delegate pattern is used extensively in iOS, and while it is generally pretty good, a delegate is essentially a pile of callbacks. A program’s readability is vastly improved when its flow can be easily followed (there’s a reason goto fell out of favor…).

Creating the Signal

Let’s say you have a few places in your app where you want to get the user’s location. There are a few delegate methods that must be implemented just to get started, as well as a few more to handle other odds and ends. But to keep things simple, let’s just create a signal that will send a value each time the location changes and also handle errors.

- (RACSignal *)monitorLocations {
    @weakify(self)
    return [RACSignal createSignal:^RACDisposable *(id  subscriber) {
        @strongify(self)
        RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable];
        
        CLLocationManager *locationManager = [CLLocationManager new];
        locationManager.delegate = self;
        
        [disposable addDisposable:[[self rac_signalForSelector:@selector(locationManager:didUpdateLocations:)
                                                  fromProtocol:@protocol(CLLocationManagerDelegate)]
                                   subscribeNext:^(RACTuple *args) {
                                       NSArray  *locations = [args second];
                                       [subscriber sendNext:[locations lastObject]];
                                   }]];
        
        [disposable addDisposable:[[self rac_signalForSelector:@selector(locationManager:didFailWithError:)
                                                  fromProtocol:@protocol(CLLocationManagerDelegate)]
                                   subscribeNext:^(RACTuple *args) {
                                       NSError *error = [args second];
                                       [subscriber sendError:error];
                                   }]];
        
        [disposable addDisposable:[[self rac_signalForSelector:@selector(locationManagerDidPauseLocationUpdates:)
                                                  fromProtocol:@protocol(CLLocationManagerDelegate)]
                                   subscribeNext:^(RACTuple *args) {
                                       [subscriber sendCompleted];
                                   }]];
        
        [disposable addDisposable:[RACDisposable disposableWithBlock:^{
            [locationManager stopUpdatingLocation];
        }]];
        
        [locationManager startUpdatingLocation];
        
        return disposable;
    }];
}

This method I’ve created returns a single signal that contains everything we need to get location updates. To avoid confusion, I’ll refer to the signal returned from this method as the monitorLocations signal.

When using createSignal, you’re responsible for a little bit more memory management than you would be if you were using the basic operators. Signals have the concept of a disposable, which helps with cleanup when a subscription ends. A disposable may hold on to some resources needed by a signal, or it may execute some code to free resources.

The monitorLocations signal contains a number of inner subscriptions, so the first thing we do is create a compound disposable. This is nothing more than a disposable that can contain more disposables.

A disposable is always returned from a subscription (e.g. subscribeNext:), although in many cases nothing further is done with it. In this case, we must add the disposable for each inner subscription to our compound disposable so that all of the subscriptions will be disposed of when the subscription to monitorLocations ends.

The next thing we do is create a new CLLocationManager. This gives each subscriber a separate location manager. You can easily have multiple subscribers in your app getting location updates without interferring with each other.

In order to pull all of our delegate methods into the monitorLocations signal, we use rac_signalForSelector:fromProtocol:. Since we assigned self as the CLLocationManager‘s delegate, this will intercept those delegate method calls, package the arguments into a RACTuple, and send the tuple on a signal.

The first delegate method that we handle is locationManager:didUpdateLocations:. The documentation for this method states that it may send multiple locations, but they’re in chronological order. Ultimately, we want our monitorLocations signal to send a location whenever a new one is available, but we’re probably only interested in the most recent one. So we grab the last object from the array and sendNext to our subscriber.

Secondly, we handle errors. The locationManager:didFailWithError: delegate method receives an NSError object, which we can simply pass along using a call to sendError on our subscriber.

Finally, we handle locationManagerDidPauseLocationUpdates:. This can happen when iOS feels like shutting down the GPS hardware, and our app is responsible for starting it up again (so it’s really more of a “stop”). This seems like a good place to round out our signal with a sendCompleted (a subscriber that is no longer interested in receiving location updates may also just unsubscribe from the monitorLocations signal).

Once that is all set up, we add one final disposable. +[RACDisposable disposableWithBlock:] is different in that instead of getting a disposable from a signal, we pass it a block that will be executed when the disposable is disposed! Whenever the monitorLocations signal is terminated (whether due to an error or completed event), we want to stop monitoring for location updates.

One other thing to notice about this final disposable is that it captures a strong reference to the locationManager we created above. Without this, the locationManager would be deallocated by ARC as soon as it went out of scope.

Finally, we can ask the locationManager to start updating location.

Implementation Details

The implementation above leaves out a few details for the sake of simplicity. If you’re expecting to get location updates in more than one place in your app, the monitorLocations signal could be extended to multicast to subscribers, stopping location updates only when all subscribers have gone away.

In addition, since we used self as the delegate, we would need to either check that the locationManager passed into each of the delegate callbacks matches the one we created, or else just use a separate instance of the class containing the monitorLocations method.

Summary

The main things to remember when using createSignal:

  • All objects created within createSignal need to be captured by a disposable, or else they’ll just be released when the variable goes out of scope.
  • Any additional subscriptions that are made need to be added to the disposable that is returned by createSignal.
  • Be sure to have your signal sendCompleted at some point, or else make it obvious that subscribers are responsible for breaking the subscription.