«reactivecocoa и mvvm» — Николай Касьянов, softwear

Post on 15-Jan-2015

1.296 Views

Category:

Documents

5 Downloads

Preview:

Click to see full reader

DESCRIPTION

В докладе расскарывается тема использования функционально-реактивного подхода для разработки iOS- и Mac-приложений, его достоинства и недостатки. Также рассказано об использовании паттерна Model-View-View Model для улучшения архитектуры и повышения тестируемости GUI-кода.

TRANSCRIPT

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 HELLvoid (^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

Model

View

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, keyPath) = signal;

• Two-way binding: RACChannel

• RACCommand

@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-frp-and-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

top related