Learn You a ReactiveCocoa for Great Good

Post on 11-May-2015

1.916 views 6 download

Tags:

description

Learn why ReactiveCocoa is more than just fancy KVO for Hipsters.

Transcript of Learn You a ReactiveCocoa for Great Good

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