«reactivecocoa и mvvm» — Николай Касьянов, softwear
Post on 15-Jan-2015
1.281 Views
Preview:
DESCRIPTION
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