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

3,549 views
3,085 views

Published on

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

2 Comments
4 Likes
Statistics
Notes
No Downloads
Views
Total views
3,549
On SlideShare
0
From Embeds
0
Number of Embeds
47
Actions
Shares
0
Downloads
50
Comments
2
Likes
4
Embeds 0
No embeds

No notes for slide

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

  1. 1. REACTIVE COCOA & MVVM Николай Касьянов
  2. 2. 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
  3. 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. 4. 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
  5. 5. 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];
  6. 6. 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
  7. 7. 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];
  8. 8. 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…
  9. 9. 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;
  10. 10. 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
  11. 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. 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. 13. 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
  14. 14. MVVM
  15. 15. View Model View Controller
  16. 16. MVVM • Model – View – View Model • An alternative to MVC • MVC is fine, but… • Meet UIViewController, The Spaghetti Monster
  17. 17. Model View Model View
  18. 18. 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
  19. 19. 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
  20. 20. YOU ALREADY CLOSER TO MVVM THAN YOU THINK Big monolithic view controllers ! External data sources, data converters & services ! MVVM
  21. 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. 22. MVVM + RAC • KVO with RACObserve • One-way binding: RAC(object, • Two-way binding: RACChannel • RACCommand keyPath) = signal;
  23. 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. 24. 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
  25. 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. 26. • Make no assumptions about a view • Expose bindable properties or signals • Expose commands to consumers • Throttle/unsubscribe signals when view is inactive
  27. 27. ANY QUESTIONS?
  28. 28. 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
  29. 29. RAC-POWERED LIBRARIES • https://github.com/octokit/octokit.objc • https://github.com/jonsterling/ReactiveFormlets • https://github.com/ReactiveCocoa/ ReactiveCocoaLayout
  30. 30. SAMPLE PROJECTS • https://github.com/AshFurrow/C-41 • https://github.com/jspahrsummers/GroceryList/ • https://github.com/corristo/SoundCloudStream

×