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…
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:
- The ability to compose operations on future data.
- An approach to minimize state and mutability.
- A declarative way to define behaviors and the relationships between properties.
- A unified, high-level interface for asynchronous operations.
- 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:
- Reactive Extensions MSDN entry
- Reactive Extensions for .NET Introduction
- Rx – Channel 9 videos
- Reactive Extensions wiki
- 101 Rx Samples
- Programming Reactive Extensions and LINQ (Co-authored by our own Paul Betts!)
Written by
Related posts
Celebrating the GitHub Awards 2024 recipients 🎉
The GitHub Awards celebrates the outstanding contributions and achievements in the developer community by honoring individuals, projects, and organizations for creating an outsized positive impact on the community.
New from Universe 2024: Get the latest previews and releases
Find out how we’re evolving GitHub and GitHub Copilot—and get access to the latest previews and GA releases.
Bringing developer choice to Copilot with Anthropic’s Claude 3.5 Sonnet, Google’s Gemini 1.5 Pro, and OpenAI’s o1-preview
At GitHub Universe, we announced Anthropic’s Claude 3.5 Sonnet, Google’s Gemini 1.5 Pro, and OpenAI’s o1-preview and o1-mini are coming to GitHub Copilot—bringing a new level of choice to every developer.