Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
ReactiveCocoa NSSpain
Search
Robert Böhnke
September 18, 2013
Programming
18
2.4k
ReactiveCocoa NSSpain
My talk for NSSpain 2013 on ReactiveCocoa
Robert Böhnke
September 18, 2013
Tweet
Share
More Decks by Robert Böhnke
See All by Robert Böhnke
Brooklyn iOS Developer Meetup February 2014
robb
8
3.1k
Cocoa Kucha Berlin 2013
robb
2
2.1k
Underscore.m + Asterism
robb
4
1.2k
ReactiveCocoa
robb
19
2.7k
Super Mario Masterclass
robb
2
320
Tetris Masterclass
robb
0
530
Other Decks in Programming
See All in Programming
[FEConf 2025] 모노레포 절망편, 14개 레포로 부활하기까지 걸린 1년
mmmaxkim
0
1.6k
Vue・React マルチプロダクト開発を支える Vite
andpad
0
110
モバイルアプリからWebへの横展開を加速した話_Claude_Code_実践術.pdf
kazuyasakamoto
0
330
Android端末で実現するオンデバイスLLM 2025
masayukisuda
1
150
アルテニア コンサル/ITエンジニア向け 採用ピッチ資料
altenir
0
100
ユーザーも開発者も悩ませない TV アプリ開発 ~Compose の内部実装から学ぶフォーカス制御~
taked137
0
180
奥深くて厄介な「改行」と仲良くなる20分
oguemon
1
540
テストカバレッジ100%を10年続けて得られた学びと品質
mottyzzz
2
590
為你自己學 Python - 冷知識篇
eddie
1
350
Deep Dive into Kotlin Flow
jmatsu
1
340
意外と簡単!?フロントエンドでパスキー認証を実現する WebAuthn
teamlab
PRO
2
750
🔨 小さなビルドシステムを作る
momeemt
4
680
Featured
See All Featured
Rails Girls Zürich Keynote
gr2m
95
14k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
32
1.6k
Understanding Cognitive Biases in Performance Measurement
bluesmoon
29
1.9k
The Cost Of JavaScript in 2023
addyosmani
53
8.9k
Bash Introduction
62gerente
615
210k
Embracing the Ebb and Flow
colly
87
4.8k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
285
13k
We Have a Design System, Now What?
morganepeng
53
7.8k
Fashionably flexible responsive web design (full day workshop)
malarkey
407
66k
[Rails World 2023 - Day 1 Closing Keynote] - The Magic of Rails
eileencodes
36
2.5k
The Success of Rails: Ensuring Growth for the Next 100 Years
eileencodes
46
7.6k
Transcript
robb
[email protected]
ceterum_censeo
speakerdeck.com/robb/ reactivecocoa-nsspain
Let's talk about ReactiveCocoa
Let's talk about ReactiveCocoa state
evil √
Have you tried turning it off and on again?
Have you tried turning it off and on again? Have
you tried turning it off and on again?
Start Okay Fail
Start Okay Fail power-cycle power-cycle
state
state
duh! " " you say
e.g.
@property (readwrite) BOOL walks; @property (readwrite) BOOL quaks; @property (readonly)
BOOL duck;
duck = walks ∧ quaks
- (void)setWalks:(BOOL)walks { _walks = walks; self.duck = walks &&
self.quacks; }
- (void)setWalks:(BOOL)walks { _walks = walks; self.duck = walks &&
self.quacks; } - (void)setQuacks:(BOOL)quacks { _quacks = quacks; self.duck = quacks && self.walks; }
- (void)setWalks:(BOOL)walks { _walks = walks; self.duck = walks &&
self.quacks; } - (void)setQuacks:(BOOL)quacks { _quacks = quacks; self.duck = quacks && self.walks; }
- (BOOL)duck { return self.walks && self.quacks; }
- (NSSet *)keyPathsForValuesAffectingDuck { return [NSSet setWithArray:@[ @"walks", @"quacks" ]];
} - (BOOL)duck { return self.walks && self.quacks; }
- (BOOL)duck { return self.walks && self.quacks; } // duck
= walks ∧ quaks
ReactiveCocoa
Functional Reactive Programming
Signals instead of variables
Pipes, not boxes
wtf? " " you may be thinking
e.g.
RAC(self, duck) = [RACSignal combineLatest:@[ RACObserve(self, walks), RACObserve(self, quacks) ]
reduce:^(NSNumber *walks, NSNumber *quacks) { return @(walks.boolValue && quacks.boolValue); }]:
RAC(self, duck) = [RACSignal combineLatest:@[ RACObserve(self, walks), RACObserve(self, quacks) ]
reduce:^(NSNumber *walks, NSNumber *quacks) { return @(walks.boolValue && quacks.boolValue); }]:
RAC(self, duck) = [RACSignal combineLatest:@[ RACObserve(self, walks), RACObserve(self, quacks) ]
reduce:^(NSNumber *walks, NSNumber *quacks) { return @(walks.boolValue && quacks.boolValue); }]:
RAC(self, duck) = [RACSignal combineLatest:@[ RACObserve(self, walks), RACObserve(self, quacks) ]
reduce:^(NSNumber *walks, NSNumber *quacks) { return @(walks.boolValue && quacks.boolValue); }]:
RAC(self, duck) = [RACSignal combineLatest:@[ RACObserve(self, walks), RACObserve(self, quacks) ]
reduce:^(NSNumber *walks, NSNumber *quacks) { return @(walks.boolValue && quacks.boolValue); }]:
// and it even does KVO! RAC(self, duck) = [RACSignal
combineLatest:@[ RACObserve(self, walks), RACObserve(self, quacks) ] reduce:^(NSNumber *walks, NSNumber *quacks) { return @(walks.boolValue && quacks.boolValue); }]:
& walks talks duck
& walks talks duck
& walks talks duck
& walks talks duck
& walks talks duck
RACSignal
-(RACDisposable * )subscribe:(id<RACSubscriber> )obj; RACSignal
<RACSubscriber>
- (void)sendNext:(id)value; <RACSubscriber>
- (void)sendNext:(id)value; - (void)sendCompleted; <RACSubscriber>
- (void)sendNext:(id)value; - (void)sendCompleted; - (void)sendError:(NSError *)error; <RACSubscriber>
so what?
Powerful toolset
map
filter
fold
write declarative code
e.g.
Your name Your email address Sign Up Newsletter
Newsletter Your name Your email address Sign Up
Newsletter Your email address Sign Up robb
Newsletter Sign Up robb robb
Newsletter Sign Up robb @robb.is Sign Up robb
Your name Your email address Sign Up validate that looks
familiar! Newsletter
- (void)viewDidLoad { [super viewDidLoad]; [self.username addTarget:self action:@selector(textFieldTextDidChange:) forControlEvents:UIControlEventAllEditingEvents]; [self.email
addTarget:self action:@selector(textFieldTextDidChange:) forControlEvents:UIControlEventAllEditingEvents]; } help!
- (void)textFieldTextDidChange:(UITextField *)field { BOOL validUsername = self.username.text.length > 0;
NSRange at = [self.email.text rangeOfString:@"@"]; BOOL validEmail = at.location != NSNotFound; self.signupButton.enabled = validUsername && validEmail; }
- (void)textFieldTextDidChange:(UITextField *)field { BOOL validUsername = self.username.text.length > 0;
NSRange at = [self.email.text rangeOfString:@"@"]; BOOL validEmail = at.location != NSNotFound; self.signupButton.enabled = validUsername && validEmail; }
ReactiveCocoa
RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ self.name.rac_textSignal self.email.rac_textSignal ] reduce:^(NSString *name,
NSString *email) { NSRange at = [email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]:
RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ self.name.rac_textSignal self.email.rac_textSignal ] reduce:^(NSString *name,
NSString *email) { NSRange at = [email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]:
RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ self.name.rac_textSignal self.email.rac_textSignal ] reduce:^(NSString *name,
NSString *email) { NSRange at = [email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]:
RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ self.name.rac_textSignal self.email.rac_textSignal ] reduce:^(NSString *name,
NSString *email) { NSRange at = [email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]:
Your name Your email address Sign Up validate Newsletter
Newsletter Your name Your email address Sign Up
Newsletter Sign Up robb
Newsletter Sign Up robb
[email protected]
Sign Up
Let's talk about
Let's talk about Asynchrony
[client logInWithSuccess:^{ [client loadMeUserWithSuccess:^(SPUser *me) { [client loadFilesForUser:me withSuccess:^(NSArray *files)
{ NSLog(@"Help me, " "I'm trapped in callback hell!"); } error:^(NSError *error) { // Handle failure } } error:^(NSError *error) { // Handle failure }]; } error:^(NSError *error) { // Handle failure }]; omg, why?!
Let's fix this
1. HTTP Client
@interface SPHTTPClient : AFHTTPClient - (RACSignal *)enqueueRequest:(NSURLRequest *)req; @end
[self enqueueHTTPRequestOperation:operation]; - (RACSignal *)enqueueRequest:(NSURLRequest *)req { return [RACSignal createSignal:^(id<RACSubscriber>
subscriber) { AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }];
[self enqueueHTTPRequestOperation:operation]; - (RACSignal *)enqueueRequest:(NSURLRequest *)req { return [RACSignal createSignal:^(id<RACSubscriber>
subscriber) { AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }];
- (RACSignal *)enqueueRequest:(NSURLRequest *)req { return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }]; [self enqueueHTTPRequestOperation:operation];
- (RACSignal *)enqueueRequest:(NSURLRequest *)req { return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }]; [self enqueueHTTPRequestOperation:operation];
- (RACSignal *)enqueueRequest:(NSURLRequest *)req { return [RACSignal createSignal:^(id<RACSubscriber> subscriber) {
AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }]; [self enqueueHTTPRequestOperation:operation];
[self enqueueHTTPRequestOperation:operation]; return [RACDisposable disposableWithBlock:^{ [operation cancel]; }]; }]; createSignal:^(id<RACSubscriber>
subscriber) { AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }];
[self enqueueHTTPRequestOperation:operation]; return [RACDisposable disposableWithBlock:^{ [operation cancel]; }]; }]; createSignal:^(id<RACSubscriber>
subscriber) { AFHTTPRequestOperation *operation; operation = [self HTTPRequestOperationWithRequest:req success:^(id operation, id response) { [subscriber sendNext:response]; [subscriber sendCompleted]; } failure:^(id operation, NSError *error) { [subscriber sendError:error]; }];
2. API interaction
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; }
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; }
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; }
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; }
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; } check out Mantle!
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; }
- (RACSignal *)fetchMeUser return [[self.client getPath:@"/me"] flattenMap:^(id JSON) { NSError
*error; SPUser *user = [SPUser userWithJSON:JSON error:&error]; if (user == nil) { return [RACSignal error:error]; } else { return [RACSignal return:user]; } }]; }
3. Putting it together
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
[[[[client logIn] then:^{ return [client fetchMeUser]; }] flattenMap:^(SPUser *me) {
return [client fetchFilesForUser:me]; }] subscribeNext:^(NSArray *files) { NSLog(@"Files: %@", files); } error:^(NSError *error) { // Handle error }];
UI&IO
UI+IO
Your name Your email address Sign Up Newsletter
Your name Your email address Sign Up validate Newsletter
Your name Your email address Sign Up validate Newsletter
, why me? oh boy
self.name.rac_textSignal self.email.rac_textSignal reduce:^(NSString *name, NSString *email) { NSRange at =
[email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]; RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ ]
self.name.rac_textSignal self.email.rac_textSignal reduce:^(NSString *name, NSString *email) { NSRange at =
[email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]; RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ ]
validName, validEmail reduce:^(NSString *name, NSString *email) { NSRange at =
[email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]; reduce:^(NSString *name, NSString *email) { NSRange at = [email rangeOfString:@"@"]; return @(at.location != NSNotFound && name.length > 0); }]; RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ ]
reduce:^(NSString *name, NSString *email) { NSRange at = [email rangeOfString:@"@"];
return @(at.location != NSNotFound && name.length > 0); }]; validName, validEmail RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ ]
reduce:^(NSNumber *name, NSNumber *email) { return @(name.boolValue && email.boolValue); }];
validName, validEmail map:^(NSString *name) { return @(name.length > 0); }]; RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ ]
RACSignal *validName = [self.name.rac_textSignal map:^(NSString *name) { return @(name.length >
0); }]; reduce:^(NSNumber *name, NSNumber *email) { validName, validEmail RAC(self.signUpButton, enabled) = [RACSignal combineLatest:@[ ] RACSignal *validEmail = [[self.email.rac_textSignal map:^(NSString *email) { return [[client validateEmail:email] startWith:@NO]; }] switchToLatest];
RACSignal *validName = [self.name.rac_textSignal map:^(NSString *name) { return @(name.length >
0); }]; RACSignal *validEmail = [[self.email.rac_textSignal map:^(NSString *email) { return [[client validateEmail:email] startWith:@NO]; }] switchToLatest];
RACSignal *validEmail = [[self.email.rac_textSignal map:^(NSString *email) { return [[client validateEmail:email]
startWith:@NO]; }] switchToLatest]; RACSignal *validName = [self.name.rac_textSignal map:^(NSString *name) { return @(name.length > 0); }];
RACSignal *validEmail = [[self.email.rac_textSignal map:^(NSString *email) { return [[client validateEmail:email]
startWith:@NO]; }] switchToLatest]; RACSignal *validName = [self.name.rac_textSignal map:^(NSString *name) { return @(name.length > 0); }];
Your name Your email address Sign Up validate Newsletter
Newsletter Your name Your email address Sign Up
Newsletter Sign Up robb
Newsletter Sign Up robb
[email protected]
Newsletter Sign Up robb
[email protected]
Sign Up
Recap
Recap + Declarative code
Recap + Declarative code + Easy handling of asynchronous tasks
Recap + Declarative code + Easy handling of asynchronous tasks
+ Great Composition
Recap + Declarative code + Easy handling of asynchronous tasks
+ Great Composition − Conceptual overhead
Recap + Declarative code + Easy handling of asynchronous tasks
+ Great Composition − Conceptual overhead − Debugging can get tricky
Recap (dtrace support coming soon) + Declarative code + Easy
handling of asynchronous tasks + Great Composition − Conceptual overhead − Debugging can get tricky
Qs?
Thanks!
iPhone 5 Setch Template courtesy of Denis Rojčyk Cloud icon
curtesy of Dmitry Baranovskiy, from The Noun Project