Learn You A ReactiveCocoa
For Great Good
Jason Larsen (@jarsen)
Software Engineer at Instructure
Reactive Cocoa
More Than Fancy KVO For Hipsters
Saying that ReactiveCocoa is just KVO/bindings
is like saying that CoreData is just a SQLite
wrapper.
Functional Programming
• No state
• No side effects
• Immutability
• First class functions
Reactive Programming
1 3
2 4
3 7 10
Reactive Programming
1 3
5 4
6 7 13
FRP
Functional Reactive Programming
RAC(self, label.text) = RACObserve(self, name);
Declarative
tell the code what to do without telling it how to do it.
Reduce State
(Not eliminate—Obj-C is not purely functional)
RACStream
RACSequence
(pull driven)
RACSignal
(push driven)
A RACStream is like a pipe—new values flow
through it. You can subscribe to these values
and then transform them as you please.
RACSequence
Pull-Driven Stream
Sequences
NSArray *numbers = @[ @1, @2, @3 ];
RACSequence *sequence = numbers.rac_sequence;
Transforming Streams
Map
RACSequence *numbersSquared = [numbers.rac_sequence
map:^id(NSNumber *number) {
return @([number intValue] * [number intValue]);
}];
Map
Mapping
Function
@[@1, @2, @3] @[@1, @4, @9]
But… for loops!
NSArray *numbers = @[@1,@2,@3];
NSMutableArray *mutableNumbers = [numbers mutableCopy];
for (NSNumber *number in numbers) {
[mutableNumbers addObject:@([number intValue] *
[number intValue])];
}
Lazy Evaluation
NSArray *strings = @[ @"A", @"B", @"C" ];
RACSequence *sequence = [strings.rac_sequence
map:^(NSString *str) {
return [str stringByAppendingString:@"_"];
}];
!
sequence.head; // evaluates @"A_"
sequence.tail.head; // evaluates @"B_"
sequence.eagerSequence; // evaluates sequence
Filter
RACSequence *evenNumbers = [numbers.rac_sequence
filter:^BOOL(NSNumber *number) {
return @([number intValue] % 2 == 0);
}];
Filter
Filtering
Function
@[@1, @2, @3] @[@2]
Fold (a.k.a. reduce)
NSNumber *sum = [numbers.rac_sequence foldLeftWithStart:@0
reduce:^id(id accumulator, id value) {
return @([accumulator intValue] + [value intValue]);
}];
Fold
Folding
Function
@[@1, @2, @3] @6
@0
Fold Left
Sequence
!
@[@1, @2, @3, @4]
@[@1, @2, @3, @4]
@[@1, @2, @3, @4]
@[@1, @2, @3, @4]
@[@1, @2, @3, @4]
Accumulator
!
@0
@1
@3
@6
@10
Combining Streams
Concatenating
// concatenate letters at end of numbers
[numbers concat:letters];
Flattening Sequences
RACSequence *sequenceOfSequences =
@[letters,numbers].rac_sequence;
!
// flattening sequences concatenates them
RACSequence *flattened = [sequenceOfSequences flatten];
RACSignal
Push-Driven Stream
Map
// set the label to always be equal to the formatted
// string of “12 units”, where 12 is whatever the
// current value of self.total
RACSignal *signal = RACObserve(self, total);
RAC(self, totalLabel.text) = [signal map:^id(NSNumber
*total) {
[NSString stringWithFormat:@"%i units", total];
}];
Filter
// only set total to the even numbers in
// the stream
RAC(self, total) = [RACSignal(self, cell1)
filter:^BOOL(NSNumber *number) {
return @([number intValue] % 2 == 0);
}];
Creating Signals
[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
NSURLSessionDataTask *task = [self PUT:path parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject) {
[subscriber sendNext:responseObject];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
Combining Signals
Yo dawg, I heard you like signals…
Sequencing
// when signal 1 completes, do signal 2
[[signal doNext:^(id x) {
NSLog(@"value: %@", x);
}]
then:^RACSignal *{
return signal2;
}];
Merging Signals
// creates a new signal that will send the
// values of both signals, complete when both
// are completed, and error when either errors
[RACSignal merge:@[signal1, signal2]];
Combine Latest Values
RACSignal *cell1Signal = RACObserve(self, cell1);
RACSignal *cell2Signal = RACObserve(self, cell2);
RAC(self, total) = [RACSignal
combineLatest:@[cell1Signal, cell2Signal] reduce:^id(id
cell1, id cell2){
return @([cell1 intValue] + [cell2 intValue]);
}];
Flattening Signals
RACSignal *signalOfSignals = [RACSignal
createSignal:^ RACDisposable *
(id<RACSubscriber> subscriber) {
[subscriber sendNext:letters];
[subscriber sendNext:numbers];
[subscriber sendCompleted];
return nil;
}];
!
// flattening signals merges them
RACSignal *flattened = [signalOfSignals
flatten];
Flattening & Mapping
// creates multiple signals of work which
// are automatically recombined, or in other words
// it maps each letter to a signal using
// saveEntriesForLetter: and then it merges them all.
letters = @[@“a”, @“b”, @“c”]
[[letters
flattenMap:^(NSString *letter) {
return [database saveEntriesForLetter:letter];
}]
subscribeCompleted:^{
NSLog(@"All database entries saved successfully.");
}];
Side Effects
Can’t live with ‘em, can’t live without ‘em
What is a “side effect?”
• logging
• making a network request
• update the UI
• changing some state somewhere
Subscriptions
[signal subscribeNext:^(id x) {
// do something with each value
} error:^(NSError *error) {
// do something with errors
} completed:^{
// do something with completed
}];
Inject Side Effects
[signal doCompleted:^{
// do some side effect after
}];
!
[signal doNext:^(id x) {
// some side effect here
}];
!
[signal doError:^(NSError *error) {
// handle error
}];
Inject Side Effects
[signal initially:^{
// do some side effect before signal
}];
Side Effects
// DO NOT DO
[signal map:^id(NSString *string) {
NSString *exclamation = [NSString
stringWithFormat:@"%@!!!", string];
[self showAlert:exclamation];
return exclamation;
}];
Side Effects
// DO DO
[[signal map:^id(NSString *string) {
return [NSString
stringWithFormat:@"%@!!!", string];
}] doNext:^(NSString *string) {
[self showAlert:string];
}];
RACSubject
Manual Signals
Use createSignal: over
RACSubject when possible
[RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber)
{
NSURLSessionDataTask *task = [client PUT:path parameters:parameters
success:^(NSURLSessionDataTask *task, id responseObject) {
[subscriber sendNext:responseObject];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
[subscriber sendError:error];
}];
return [RACDisposable disposableWithBlock:^{
[task cancel];
}];
}];
Concurrency
Scenario
• I want to run three networking calls and then when
they are all done do something
• I want to MERGE three signals and THEN do
something.
Scenario Solution
// basically: merging signals can replace dispatch groups
[[RACSignal merge:@[signal1, signal2, signal3]] then:^RACSignal * {
return [self someSignalToDoAfter];
}];
Replay/Multicasting
Replace Delegation
• rac_signalForSelector:fromProtocol:
Other Cool Methods
• -throttle:
• -takeUntil: can be used to automatically dispose of a
subscription when an event occurs (like a "Cancel"
button being pressed in the UI).
• -setNameWithFormat: for debugging
• -logNext, -logError, -logCompleted, -logAll automatically
log signal events as they occur, and include the name
of the signal in the messages. This can be used to
conveniently inspect a signal in real-time.
Your hammer
99% of the time
• -subscribeNext:error:completed:
• -doNext: / -doError: / -doCompleted:
• -map:
• -filter:
• -concat:
• -flattenMap:
• -then:
• +merge:
• +combineLatest:reduce:
• -switchToLatest:
We Want More!
Specifically, read DesignGuidelines.md
and BasicOperators.md
https://github.com/ReactiveCocoa/
ReactiveCocoa/tree/master/Documentation
https://leanpub.com/iosfrp
If you’re into books, this one’s decent.
https://github.com/ReactiveCocoa/
ReactiveViewModel
Github’s Obj-C API Lib
https://github.com/octokit/octokit.objc
!
(and Instructure’s API Lib modeled after it)
https://github.com/instructure/canvaskit
Don’t Cross the Streams
Type Safety
A word of warning
Non KVO Compliance
like textLabel.text
viewDidLoad Bloat
break stuff up into setupMethods

Learn You a ReactiveCocoa for Great Good