Johan Sørensen

Tiny bits and pieces on ReactiveCocoa

When ReactiveCocoa was released last year I gave it a quick look, but it didn’t really click with me, which made me feel dumb so I made the usual mistake of dismissing it.
(sidenote: in a previous life I learnt the hard way to not dismiss the work of Github, but that’s an entirely different story and circumstances. Old dog. No new tricks).

A few weeks ago I decided to give it a proper look and integrate it into Sequence. This post isn’t meant as a tutorial or even an introduction, it’s just a collection of things that weren’t immediately obvious to me. I suggest reading this and this, as well as the official documentation, which does a better job than me at introducing the high-level concepts.

Use RAC() and RACAble() to easily bridge between worlds

RAC() lets you assign a property to a signal, meaning that whenever the signal sends its next event it’ll set the property to the value of that signal.


// @property (nonatomic, assign) BOOL edited;
RAC(self.edited) = [textChanged map:^id(NSString *txt){
    return @([text length] > 0);
}];

RACAble() as well as RACAbleWithStart() creates a signal which acts as an observer for the given key path. RACAbleWithStart simply starts with the current value as well. I’ve been using this a lot instead of the traditional cocoa KVO:


// self.edited will be true whenever self.text is changed to be non-empty
RAC(self.edited) = [RACAble(self.text) map:^id(NSString *txt){
    return @([text length] > 0);
}];

Further bridging between the reactive and the non-reactive worlds can be done with rac_liftSelector:withObject:, such as setting the state of a NSButton based on a signal:


RACSignal *inspectorVisible = [RACAbleWithStart(self.timelineViewController.isTrackInspectorVisible) map:^id(NSNumber *visible) {
    return @([visible boolValue] ? NSOnState : NSOffState);
}];
[self.toggleTrackInspectorButton rac_liftSelector:@selector(setState:) withObjects:inspectorVisible];

Of course, the real power of ReactiveCocoa is that signals are composable, looking at our textChanged signal again we can throttle the rate at which we receive the signal, for instance to avoid unnecessary network requests (think autocompletion of remote values) or keeping UI updates reasonable


// only update self.edited if the textChanged signal hasn't sent any events in the last 0.3 seconds
RAC(self.edited) = [[textChanged throttle:0.3] map:^id(NSString *txt){
    return @([text length] > 0);
}];

Use RACSubject for bridging between worlds in a controlled manner

RACSubject is a signal which you can manually control. This is extremely useful when bridging between worlds and you want to control when the signal is sent (as opposed to just when a property changes). For instance in Sequence I manually send a signal through a RACSubject whenever the marker is moved in the timeline. My timeline class simply exposes the RACSubject as a RACSignal externally:


// JKSTimelineView.h
@interface JKSTimelineView : NSView
@property (nonatomic, strong, readonly) RACSignal *frameChanged;
// ...
@end


// JKSTimelineView.m
@interface JKSTimelineView ()
@property (nonatomic, strong, readonly) RACSubject *frameSubject;
@end

@implementation JKSTimelineView
- (RACSignal *)frameChanged
{
    return [[self.frameSubject distinctUntilChanged] startWith:@(0)];
}

#pragma mark - Private methods

- (void)moveMarkerToFrame:(NSInteger)frameIndex
{
    // ...
    [self.frameSubject sendNext:@([self currentFrameIndex])];
}
@end

I return the internal frameSubject from the frameChanged property, thereby hiding the implementation detail of the fact that I use a RACSubject internally. Furthermore, frameChanged starts by sending 0 and only sends events if the value of the signal actually changes.

A useful subclass of RACSubject is RACReplaySubject, which stores any values sent to it and replays it to new subscribers. This is very useful for network requests or other heavy work you only need to do once.

Using it

It took a while for me to get ReactiveCocoa and conceptually it’s very different to the style of imperative programming I usually do. But even though the abstractions seem high I certainly think it’s been worth it integrating it into Sequence.

I admit I don’t currently harness the full power of ReactiveCocoa, as I don’t do a lot of signal composing. I’ve been meaning to refactor parts of the rendering pipeline in Sequence to use ReactiveCocoa, as it really could benefit of a cleaner abstracting of the inter-dependent steps it needs to run through in order to analyze and apply the deflickering to the photos. But I’m starting slow, replacing the UI related parts that previously relied on bindings and KVO to use signals instead and overall I think it comes out nicer and easier to understand and work with, provided you know how ReactiveCocoa actually works of course.

As always, the best way to learn something is to just jump in and use it in a real project.


Other recent entries:

Introducing Sequence, the Mac timelapse editor
Back to the Mac
Modern modern Objective-C
Pausing and controlling the speed of Core Animation

more…