REACTIVE COCOA & MVVM
Николай Касьянов
REACTIVE COCOA
•

Objective-C framework for processing and composing streams	


•

Unifies async Cocoa patterns: callbacks, delegates, KVO,
notifications	


•

Very composable	


•

Helps to minimize state	


•

Inspired by Reactive Extensions for .NET
CALLBACK HELL
void (^completion)(UIImage *) = ...
id token = [self loadImage:url completion:^(NSData *data, NSError *error) {
if (data == nil) {
completion(defaultImage);
}
else {
[self unpackImageFromData:data completion:^(UIImage *image) {
if (image == nil) { // unpacking failed
completion(defaultImage);
}
else {
completion(image);
}
}]
}
}];

!

// client code
[imageLoader cancel:token];

!
FUTURES
•

Future can either complete with a value or reject
with an error	


•

JavaScript Promises/A+	


•

There are some Objective-C implementations	


•

RAC can into futures too
RACSignal *image =
[[[self rac_imageFromURL:url] flattenMap:^(NSData *data) {
return [self rac_unpackedImageFromData:data];
}]
catchTo:[RACSignal return:defaultImage]];

!

// client code:
RAC(cell.imageView, image) =
[image takeUntil:cell.rac_prepareForReuseSignal];
RACSignal
•

A stream of values	


•

One can subscribe to new value, error or completion	


•

Supports functional constructs: map, filter, flatMap, reduce
etc	


•

Сold or hot	


•

A monad
RACSignal *allPosts =
[RACSignal createSignal:^(id <RACSubscriber> s) {
[httpClient GET:@"/posts.json" success:^(NSArray *posts) {
[s sendNext:posts];
[s sendCompleted];
} failure:^(NSError *error) {
[s sendError:error];
}];

!

!

}];

return [RACDisposable disposableWithBlock:^{
[operation cancel];
}];

RACSignal *posts =
[[allPosts flattenMap:^(NSArray *items) {
return items.rac_signal;
}] filter:^(Post *post) {
return post.hasComments;
}];
// Nothing happened yet

!

[[posts collect] subscribeNext:^(NSArray *items) { NSLog(@"Posts: %@", items) }];

!

RACDisposable *disposable =
[posts subscribeCompleted:^{ NSLog(@"Done"); }];
// Ooops network request performed twice :(

!

[disposable dispose];
STREAMS
•

Powerful abstraction	


•

Streams for futures is like iterables for scalar values	


•

Umbrella concept for asynchronous Cocoa
patterns	


•

You can even use signals as values. Yo dawg…
RACSignal *searchResults =
[[[[textField.rac_textSignal throttle:1] filter:^(NSString *query) {
return query.length > 2;
}] map:^(NSString *query) {
return [[[networkClient itemsMatchingQuery:query]
doError:^(NSError *error) {
[self displayError:error];
}]
catchTo:[RACSignal empty]];
}] switchToLatest];

!

// Automatically disposes subscription on self deallocation
// cancelation running network request (if any)
RAC(self, items) = searchResults;
DEALING WITH IMPERATIVE
API
•

Property binding (one-way and two-way)	


•

Selector lifting (-rac_liftSelector:withSignals:)	


•

Signals from selector (-rac_signalForSelector:)	


•

Operators for injecting side-effects: doNext, doError,
initially, finally
RACSignal *range = [[[RACSignal merge:@[
[self rac_signalForSelector:@selector(tableView:willDisplayCell...)]
[self rac_signalForSelector:@selector(tableView:didEndDisplayingCell...)]
]] map:^(RACtuple *args) {
UITableView *tableView = args[0];
NSArray *indexPaths = tableView.indexPathsForVisibleRows;
NSRange range = NSMakeRange([indexPaths[0] row], indexPaths.count);
return [NSValue valueWithRange:range];
}];

!

[self rac_liftSelector:@selector(updateVisibleRange:)
withSignals:range, nil];
CONCURRENCY
•

RACScheduler

•

-deliverOn:

and -subscribeOn:

- (RACSignal *)itemsFromDB {
return [RACSignal createSignal:^(id <RACSubscriber> subscriber) {
NSArray *items = [sqliteWrapper fetchItemsFromTable:@"posts"];

!

[subscriber sendNext:items];
[subscriber sendCompleted];

!
}

!

}];

return nil;

[[[[self itemsFromDB]
subscribeOn:[RACScheduler scheduler]]
deliverOn:[RACScheduler mainThreadScheduler]]
subscribeNext:^(NSArray *items) {
self.items = items;
}];
ISSUES
•

Steep learning curve	


•

Runtime overhead	


•

Tricky debugging: crazy call stacks, lots of asynchrony,
retain cycles	


•

You’ll need to deal with imperative Cocoa API anyway	


•

Losing last bits of type information
MVVM
View

Model

View
Controller
MVVM
•

Model – View – View Model	


•

An alternative to MVC	


•

MVC is fine, but…	


•

Meet UIViewController, The Spaghetti Monster
Model

View
Model

View
MVVM
•

MVVM knows nothing about a view	


•

MVVM uses underlying layers (persistence, web
service clients, cache) to populate view data	


•

You can even use it with ncurses
WHY MVVM?
•

Clear separation between view and presentation logiс	


•

Reusability across different views and even platforms	


•

Testability	


•

View models are models	


•

Persistence can be hidden behind view model
YOU ALREADY CLOSER TO
MVVM THAN YOU THINK
Big monolithic view controllers	

!

External data sources, data converters & services	

!

MVVM
@interface UserRepositoryViewModel : NSObject

!

// KVOable properties
@property (nonatomic, copy, readonly) NSArray *items;
@property (nonatomic, strong, readonly) User *selectedUser;
@property (nonatomic, readonly) BOOL isLoading;

!

- (void)refreshItems;
- (void)selectItemAtIndex;

!

@end
MVVM + RAC
•

KVO with RACObserve	


•

One-way binding: RAC(object,

•

Two-way binding: RACChannel	


•

RACCommand

keyPath) = signal;
@interface LoginViewModel : NSObject

!

@property (nonatomic, copy) NSString *login;
@property (nonatomic, copy) NSString *password;

!

@property (nonatomic, strong, readonly) RACCommand *login;

!

@end

!
!

// Somewhere in view controller
id viewTerminal = loginField.rac_newTextChannel;
id modelTerminal = RACChannelTo(viewModel, login);

!

[[viewTerminal map:^(NSString *value) {
return [value lowercaseString];
}] subscribe:modelTerminal];
[modelTerminal subscribe:viewTerminal];

!

loginButton.rac_command = viewModel.login;
[self rac_liftSelector:@selector(displayError:)
withSignals:viewModel.login.errors, nil];

!

RAC(spinner, animating) = viewModel.login.executing;
RACCommand
•

Runs signal block on -execute: and subscribes to its result	


•

Multicasts execution signals to consumers	


•

Multicasts inner signal errors to consumers	


•

Enabled/disabled state can be controlled by a bool signal	


•

Exposes execution state (running/not running)	


•

Can be bound to UI control
RACSignal *networkReachable = RACObserve(httpClient, reachable);

!

RACCommand *login = [[RACCommand alloc]
initWithEnabled:networkReachable
signalBlock:^(id _) {
// Boolean signal, sends @YES on success
return [self.backendClient loginWithLogin:self.login
password:self.password];
}];

!

RAC(self, loggedIn) = [[login.executionSignals switchToLatest]
startWith:@NO];
•

Make no assumptions about a view	


•

Expose bindable properties or signals	


•

Expose commands to consumers	


•

Throttle/unsubscribe signals when view is
inactive
ANY QUESTIONS?
LINKS
•

https://github.com/ReactiveCocoa/ReactiveCocoa/	


•

https://github.com/ReactiveCocoa/ReactiveViewModel	


•

http://cocoamanifest.net/articles/2013/10/mvc-mvvm-frpand-building-bridges.html	


•

https://rx.codeplex.com	


•

http://netflix.github.io/RxJava/javadoc/rx/Observable.html
RAC-POWERED LIBRARIES
•

https://github.com/octokit/octokit.objc	


•

https://github.com/jonsterling/ReactiveFormlets	


•

https://github.com/ReactiveCocoa/
ReactiveCocoaLayout
SAMPLE PROJECTS
•

https://github.com/AshFurrow/C-41	


•

https://github.com/jspahrsummers/GroceryList/	


•

https://github.com/corristo/SoundCloudStream

«ReactiveCocoa и MVVM» — Николай Касьянов, SoftWear

  • 1.
    REACTIVE COCOA &MVVM Николай Касьянов
  • 2.
    REACTIVE COCOA • Objective-C frameworkfor processing and composing streams • Unifies async Cocoa patterns: callbacks, delegates, KVO, notifications • Very composable • Helps to minimize state • Inspired by Reactive Extensions for .NET
  • 3.
    CALLBACK HELL void (^completion)(UIImage*) = ... id token = [self loadImage:url completion:^(NSData *data, NSError *error) { if (data == nil) { completion(defaultImage); } else { [self unpackImageFromData:data completion:^(UIImage *image) { if (image == nil) { // unpacking failed completion(defaultImage); } else { completion(image); } }] } }]; ! // client code [imageLoader cancel:token]; !
  • 4.
    FUTURES • Future can eithercomplete with a value or reject with an error • JavaScript Promises/A+ • There are some Objective-C implementations • RAC can into futures too
  • 5.
    RACSignal *image = [[[selfrac_imageFromURL:url] flattenMap:^(NSData *data) { return [self rac_unpackedImageFromData:data]; }] catchTo:[RACSignal return:defaultImage]]; ! // client code: RAC(cell.imageView, image) = [image takeUntil:cell.rac_prepareForReuseSignal];
  • 6.
    RACSignal • A stream ofvalues • One can subscribe to new value, error or completion • Supports functional constructs: map, filter, flatMap, reduce etc • Сold or hot • A monad
  • 7.
    RACSignal *allPosts = [RACSignalcreateSignal:^(id <RACSubscriber> s) { [httpClient GET:@"/posts.json" success:^(NSArray *posts) { [s sendNext:posts]; [s sendCompleted]; } failure:^(NSError *error) { [s sendError:error]; }]; ! ! }]; return [RACDisposable disposableWithBlock:^{ [operation cancel]; }]; RACSignal *posts = [[allPosts flattenMap:^(NSArray *items) { return items.rac_signal; }] filter:^(Post *post) { return post.hasComments; }]; // Nothing happened yet ! [[posts collect] subscribeNext:^(NSArray *items) { NSLog(@"Posts: %@", items) }]; ! RACDisposable *disposable = [posts subscribeCompleted:^{ NSLog(@"Done"); }]; // Ooops network request performed twice :( ! [disposable dispose];
  • 8.
    STREAMS • Powerful abstraction • Streams forfutures is like iterables for scalar values • Umbrella concept for asynchronous Cocoa patterns • You can even use signals as values. Yo dawg…
  • 9.
    RACSignal *searchResults = [[[[textField.rac_textSignalthrottle:1] filter:^(NSString *query) { return query.length > 2; }] map:^(NSString *query) { return [[[networkClient itemsMatchingQuery:query] doError:^(NSError *error) { [self displayError:error]; }] catchTo:[RACSignal empty]]; }] switchToLatest]; ! // Automatically disposes subscription on self deallocation // cancelation running network request (if any) RAC(self, items) = searchResults;
  • 10.
    DEALING WITH IMPERATIVE API • Propertybinding (one-way and two-way) • Selector lifting (-rac_liftSelector:withSignals:) • Signals from selector (-rac_signalForSelector:) • Operators for injecting side-effects: doNext, doError, initially, finally
  • 11.
    RACSignal *range =[[[RACSignal merge:@[ [self rac_signalForSelector:@selector(tableView:willDisplayCell...)] [self rac_signalForSelector:@selector(tableView:didEndDisplayingCell...)] ]] map:^(RACtuple *args) { UITableView *tableView = args[0]; NSArray *indexPaths = tableView.indexPathsForVisibleRows; NSRange range = NSMakeRange([indexPaths[0] row], indexPaths.count); return [NSValue valueWithRange:range]; }]; ! [self rac_liftSelector:@selector(updateVisibleRange:) withSignals:range, nil];
  • 12.
    CONCURRENCY • RACScheduler • -deliverOn: and -subscribeOn: - (RACSignal*)itemsFromDB { return [RACSignal createSignal:^(id <RACSubscriber> subscriber) { NSArray *items = [sqliteWrapper fetchItemsFromTable:@"posts"]; ! [subscriber sendNext:items]; [subscriber sendCompleted]; ! } ! }]; return nil; [[[[self itemsFromDB] subscribeOn:[RACScheduler scheduler]] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSArray *items) { self.items = items; }];
  • 13.
    ISSUES • Steep learning curve • Runtimeoverhead • Tricky debugging: crazy call stacks, lots of asynchrony, retain cycles • You’ll need to deal with imperative Cocoa API anyway • Losing last bits of type information
  • 14.
  • 15.
  • 16.
    MVVM • Model – View– View Model • An alternative to MVC • MVC is fine, but… • Meet UIViewController, The Spaghetti Monster
  • 17.
  • 18.
    MVVM • MVVM knows nothingabout a view • MVVM uses underlying layers (persistence, web service clients, cache) to populate view data • You can even use it with ncurses
  • 19.
    WHY MVVM? • Clear separationbetween view and presentation logiс • Reusability across different views and even platforms • Testability • View models are models • Persistence can be hidden behind view model
  • 20.
    YOU ALREADY CLOSERTO MVVM THAN YOU THINK Big monolithic view controllers ! External data sources, data converters & services ! MVVM
  • 21.
    @interface UserRepositoryViewModel :NSObject ! // KVOable properties @property (nonatomic, copy, readonly) NSArray *items; @property (nonatomic, strong, readonly) User *selectedUser; @property (nonatomic, readonly) BOOL isLoading; ! - (void)refreshItems; - (void)selectItemAtIndex; ! @end
  • 22.
    MVVM + RAC • KVOwith RACObserve • One-way binding: RAC(object, • Two-way binding: RACChannel • RACCommand keyPath) = signal;
  • 23.
    @interface LoginViewModel :NSObject ! @property (nonatomic, copy) NSString *login; @property (nonatomic, copy) NSString *password; ! @property (nonatomic, strong, readonly) RACCommand *login; ! @end ! ! // Somewhere in view controller id viewTerminal = loginField.rac_newTextChannel; id modelTerminal = RACChannelTo(viewModel, login); ! [[viewTerminal map:^(NSString *value) { return [value lowercaseString]; }] subscribe:modelTerminal]; [modelTerminal subscribe:viewTerminal]; ! loginButton.rac_command = viewModel.login; [self rac_liftSelector:@selector(displayError:) withSignals:viewModel.login.errors, nil]; ! RAC(spinner, animating) = viewModel.login.executing;
  • 24.
    RACCommand • Runs signal blockon -execute: and subscribes to its result • Multicasts execution signals to consumers • Multicasts inner signal errors to consumers • Enabled/disabled state can be controlled by a bool signal • Exposes execution state (running/not running) • Can be bound to UI control
  • 25.
    RACSignal *networkReachable =RACObserve(httpClient, reachable); ! RACCommand *login = [[RACCommand alloc] initWithEnabled:networkReachable signalBlock:^(id _) { // Boolean signal, sends @YES on success return [self.backendClient loginWithLogin:self.login password:self.password]; }]; ! RAC(self, loggedIn) = [[login.executionSignals switchToLatest] startWith:@NO];
  • 26.
    • Make no assumptionsabout a view • Expose bindable properties or signals • Expose commands to consumers • Throttle/unsubscribe signals when view is inactive
  • 27.
  • 28.
  • 29.
  • 30.