Learn You A ReactiveCocoaFor Great Good
Jason Larsen (@jarsen)Software Engineer at Instructure
Reactive CocoaMore 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
FRPFunctional Reactive Programming
RAC(self, label.text) = RACObserve(self, name);
Declarativetell 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.
RACSequencePull-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 LeftSequence
!@[@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];
RACSignalPush-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 SignalsYo 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 SignalsRACSignal *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 EffectsCan’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]; }];
RACSubjectManual 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 SafetyA word of warning
Non KVO Compliancelike textLabel.text
viewDidLoad Bloatbreak stuff up into setupMethods
Top Related