Skip to content

ReactiveCocoa for a better world

Native apps spend a lot of time waiting and then reacting. We wait for the user to do something in the UI. Wait for a network call to respond. Wait…

Author

Native apps spend a lot of time waiting and then reacting. We wait for the user to do something in the UI. Wait for a network call to respond. Wait for an asynchronous operation to complete. Wait for some dependent value to change. And then they react.

But all those things—all that waiting and reacting—is usually handled in many disparate ways. That makes it hard for us to reason about them, chain them, or compose them in any uniform, high-level way. We can do better.

That’s why we’ve open-sourced a piece of the magic behind GitHub for Mac: ReactiveCocoa (RAC).

RAC is a framework for composing and transforming sequences of values.

No seriously, what is it?

Let’s get more concrete. ReactiveCocoa gives us a lot of cool stuff:

  1. The ability to compose operations on future data.
  2. An approach to minimize state and mutability.
  3. A declarative way to define behaviors and the relationships between properties.
  4. A unified, high-level interface for asynchronous operations.
  5. A lovely API on top of KVO.

Those all might seem a little random until you realize that RAC is all about handling these cases where we’re waiting for some new value and then reacting.

The real beauty of RAC is that it can adapt to a lot of different, commonly-encountered scenarios.

Enough talk. Let’s see what it actually looks like.

Examples

RAC can piggyback on KVO (key-value observing) to give us a sequence of values from a KVO-compliant property. For example, we can watch for changes to our username property:

[RACAble(self.username) subscribeNext:^(NSString *newName) {
    NSLog(@"%@", newName);
}];

That’s cool, but it’s really just a nicer API around KVO. The really cool stuff happens when we compose sequences to express complex behavior.

Let’s suppose we want to check if the user entered a specific username, but only if it’s within the first three values they entered:

[[[[RACAble(self.username)
    distinctUntilChanged]
    take:3]
    filter:^(NSString *newUsername) {
        return [newUsername isEqualToString:@"joshaber"];
    }]
    subscribeNext:^(id _) {
        NSLog(@"Hi me!");
    }];

We watch username for changes, filter out non-distinct changes, take only the first three non-distinct values, and then if the new value is “joshaber”, we print out a nice welcome.

So what?

Think about what we’d have to do to implement that without RAC. We’d have to:

  • Use KVO to add an observer for username.
  • Add a property to remember the last value we got through KVO so we could ignore non-distinct changes.
  • Add a property to count how many non-distinct values we’d received.
  • Increment that property every time we got a non-distinct value
  • Do the actual comparison.

RAC lets us do the same thing with less state, less boilerplate, better code locality, and better expression of our intent.

What else?

We can combine sequences:

[[RACSignal
    combineLatest:@[ RACAble(self.password), RACAble(self.passwordConfirmation) ]
    reduce:^(NSString *currentPassword, NSString *currentConfirmPassword) {
        return [NSNumber numberWithBool:[currentConfirmPassword isEqualToString:currentPassword]];
    }]
    subscribeNext:^(NSNumber *passwordsMatch) {
        self.createEnabled = [passwordsMatch boolValue];
    }];

Any time our password or passwordConfirmation properties change, we combine the latest values from both and reduce them to a BOOL of whether or not they matched. Then we enable or disable the create button with that result.

Bindings

We can adapt RAC to give us powerful bindings with conditions and transformations:

[self
    rac_bind:@keypath(self.helpLabel.text)
    to:[[RACAble(self.help)
        filter:^(NSString *newHelp) {
            return newHelp != nil;
        }]
        map:^(NSString *newHelp) {
            return [newHelp uppercaseString];
        }]];

That binds our help label’s text to our help property when the help property isn’t nil and after uppercasing the string (because users love being YELLED AT).

Async

RAC also fits quite nicely with async operations.

For example, we can call a block once multiple concurrent operations have completed:

[[RACSignal
    merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]]
    subscribeCompleted:^{
        NSLog(@"They're both done!");
    }];

Or chain async operations:

[[[[client
    loginUser]
    flattenMap:^(id _) {
        return [client loadCachedMessages];
    }]
    flattenMap:^(id _) {
        return [client fetchMessages];
    }]
    subscribeCompleted:^{
        NSLog(@"Fetched all messages.");
    }];

That will login, load the cached messages, then fetch the remote messages, and then print “Fetched all messages.”

Or we can trivially move work to a background queue:

[[[[[client
    fetchUserWithUsername:@"joshaber"]
    deliverOn:[RACScheduler scheduler]]
    map:^(User *user) {
        // this is on a background queue
        return [[NSImage alloc] initWithContentsOfURL:user.avatarURL];
    }]
    deliverOn:RACScheduler.mainThreadScheduler]
    subscribeNext:^(NSImage *image) {
        // now we're back on the main queue
        self.imageView.image = image;
    }];

Or easily deal with potential race conditions. For example, we could update a property with the result of an asynchronous call, but only if the property doesn’t change before the async call completes:

[[[self
    loadDefaultMessageInBackground]
    takeUntil:RACAble(self.message)]
    toProperty:@keypath(self.message) onObject:self];

How does it work?

RAC is fundamentally pretty simple. It’s all signals all the way down. _(Until you reach turtles.)_

Subscribers subscribe to signals. Signals send their subscribers ‘next’, ‘error’, and ‘completed’ events. So if it’s all just signals sending events, the key question becomes when do those events get sent?

Creating signals

Signals define their own behavior with respect to if and when events are sent. We can create our own signals using +[RACSignal createSignal:]:

RACSignal *helloWorld = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    [subscriber sendNext:@"Hello, "];
    [subscriber sendNext:@"world!"];
    [subscriber sendCompleted];
    return nil;
}];

The block we give to +[RACSignal createSignal:] is called whenever the signal gets a new subscriber. The new subscriber is passed into the block so that we can then send it events. In the above example, we created a signal that sends “Hello, “, and then “world!”, and then completes.

Nesting signals

We could then create another signal based off our helloWorld signal:

RACSignal *joiner = [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
    NSMutableArray *strings = [NSMutableArray array];
    return [helloWorld subscribeNext:^(NSString *x) {
        [strings addObject:x];
    } error:^(NSError *error) {
        [subscriber sendError:error];
    } completed:^{
        [subscriber sendNext:[strings componentsJoinedByString:@""]];
        [subscriber sendCompleted];
    }];
}];

Now we have a joiner signal. When someone subscribes to joiner, it subscribes to our helloWorld signal. It adds all the values it receives from helloWorld and then when helloWorld completes, it joins all the strings it received into a single string, sends that, and completes.

In this way, we can build signals on each other to express complex behaviors.

RAC implements a set of operations on RACSignal that do exactly that. They take the source signal and return a new signal with some defined behavior.

More info

ReactiveCocoa works on both Mac and iOS. See the README for more info and check out the Mac demo project for some practical examples.

For .NET developers, this all might sound eerily familiar. ReactiveCocoa essentially an Objective-C version of .NET’s Reactive Extensions (Rx).

Most of the principles of Rx apply to RAC as well. There are some really good Rx resources out there:

Explore more from GitHub

Engineering

Engineering

Posts straight from the GitHub engineering team.
The ReadME Project

The ReadME Project

Stories and voices from the developer community.
GitHub Copilot

GitHub Copilot

Don't fly solo. Try 30 days for free.
Work at GitHub!

Work at GitHub!

Check out our current job openings.