Article summary
ReactiveCocoa is a functional-reactive framework for Objective-C that can be leveraged to manage the flow of data within an application. The API provides tools that enable the composition of complex streams that chain data sources to data consumers. Typically, data travels down these streams in one direction—from source to consumer—but on occasion, it’s necessary to have data travel in both directions. This is where the RACChannel feature can be used.
How it Works
A RACChannel is an object that supports bi-directional data binding. A channel consists of two terminals that are essentially the end-points of the channel. These terminals (RACChannelTerminal) are fancy signals that are also subscribers. Values can be sent on them, like a RACSubject, but they can also subscribe to other signals.
The magic of a channel is how the two terminals work together. When a value is sent on one channel, that value is received by the subscribers of the other channel. This allows data to be sent in either direction without circling back and forth forever.
One case where this might be useful is when you have a property in a view control that reflects the value of a property in a model. If the user interacts with the control and changes the value of the property, you want the model to receive the update. Likewise, if the data in the model changes, you want the view to be updated to reflect the new value.
Example #1
I put together a couple of silly examples to demonstrate the power of channels and terminals. The first one is very simple. It’s a view that contains two UITextFields. The text properties of the two fields are linked with a channel so that changes made to one are reflected in the other. The net effect is shown below.
This is accomplished with just four lines of code:
RACChannelTerminal *textField1Channel = [view.textField1 rac_newTextChannel];
RACChannelTerminal *textField2Channel = [view.textField2 rac_newTextChannel];
[textField1Channel subscribe:textField2Channel];
[textField2Channel subscribe:textField1Channel];
ReactiveCocoa includes an extension of UITextField class that provides a helper method, rac_newTextChannel, which creates a channel for the text property and returns a pointer to one of the terminals. The example code above creates a channel for each control and subscribes the resulting terminals to each other. This causes the input text to be mirrored in the other field.
Example #2
The next example is similar but demonstrates how the data sent through a channel can be manipulated before it is consumed. I created a slider that mimics a temperature setting. When the temperature is set above a threshold, the heat turns on. When it is set below a lower threshold, the A/C turns on. Also, the heat and A/C can be forced on, which causes the temperature setting to be adjusted appropriately.
To accomplish this I created three channels: one for the slider and one for each switch. Again, helper macros create the channels that bind one terminal to the desired property and return a pointer to the other terminal.
The values sent by the switches are mapped to temperature values that will be set when the switches are toggled. Since there is only one temperature, the results are merged. The resulting signal is then subscribed to by the slider terminal.
RACChannelTerminal *sliderTerminal = [view.slider rac_newValueChannelWithNilValue:@0];
RACChannelTerminal *heatSwitchTerminal = view.heatSwitch.rac_newOnChannel;
RACChannelTerminal *acSwitchTerminal = view.acSwitch.rac_newOnChannel;
RACSignal *forceHeatSignal = [heatSwitchTerminal map:^id(id value) {
NSLog(@"heatSwitch sent %@", value);
return [value boolValue] ? minHeatTemp : @([minHeatTemp floatValue] - 1);
}];
RACSignal *forceACSignal = [acSwitchTerminal map:^id(id value) {
NSLog(@"acSwitch sent %@", value);
return [value boolValue] ? maxACTemp : @([maxACTemp floatValue] + 1);
}];
[[RACSignal merge:@[forceHeatSignal, forceACSignal]] subscribe:sliderTerminal];
The sliderTerminal is then subscribed to—twice—and mapped to boolean to values that indicate if the temperature has exceeded the heat or A/C thresholds. The resulting signals are subscribed to by the switch terminals. This causes the switches to get updated when the slider moves.
RACSignal *heatOnSignal = [[sliderTerminal map:^id(id value) {
return @([value floatValue] > [minHeatTemp floatValue]);
}] merge:[[acSwitchTerminal not] ignore:@YES]];
[heatOnSignal subscribe:heatSwitchTerminal];
RACSignal *acOnSignal = [[sliderTerminal map:^id(id value) {
return @([value floatValue] < [maxACTemp floatValue]);
}] merge:[[heatSwitchTerminal not] ignore:@YES]];
[acOnSignal subscribe:acSwitchTerminal];
RAC(view.heatLabel, text) = [[heatOnSignal merge:heatSwitchTerminal] map:^id(id value) {
return [value boolValue] ? @"Heat On" : @"Heat Off";
}];
RAC(view.acLabel, text) = [[acOnSignal merge:acSwitchTerminal] map:^id(id value) {
return [value boolValue] ? @"A/C On" : @"A/C Off";
}];
If you read the code carefully, you’ll notice that I merged the signals from the switches into the acOnSignal and heatOnSignal. This ensures that both switches can’t be on at the same time. This is necessary because when one switch changes, the value on the slider is updated programmatically, but the slider does not trigger an event. If it did, setting control properties programmatically would often result in infinite callback loops.
Earlier I mentioned that RACChannels are useful for synchronizing updates to a property in a view model. In the examples above I only demonstrated how to bind a channel to properties in view controls using helpers like rac_newTextChannel. To bind a channel to any class property you use the RACChannelTo macro like this:
RACChannelTerminal *integerChannel = RACChannelTo(self, integerProperty, @42);
Conclusions
RACChannels and RACChannelTerminals are interesting and complex structures. They can be extremely handy in certain circumstances but also quite frustrating if you don’t have a clear understanding of how they work. When considering using them, keep in mind that they are very tightly coupled with object properties. If you are trying to bind to something that is only accessible via methods—like the selected row in a table—it’s probably better to pursue a different approach.
You can access the full source code for both of the examples here: