reactivecocoa - impathic bindings vs. reactivecocoa
TRANSCRIPT
ReactiveCocoa
Marc Prud'hommeaux <[email protected]> Ottawa CocoaHeads February 13th, 2014
Introduction
ReactiveCocoa
• Functional Reactive Programming
• Open-source library by Josh Abernathy & Justin Spahr-Summers of GitHub
• Modeled on Microsoft's Reactive Extensions for .NET
Features
• The ability to compose operations on future data.
• An approach to minimize state and mutability.
• A declarative way to define behaviors and the relationships between properties.
• A unified, high-level interface for asynchronous operations.
• An API on top of KVO
State Changes are Signals
• Anyone can "subscribe" to a "signal"
• A signal can be:
• Tapping a UIButton
• Receiving data on a NSURLConnection
• A global event in NSNotificationCenter
• ... and any other future state change
Less Spaghetti!
Features (summary)
Functional Reactive Programming
• First suggested in ICFP 97 paper Functional Reactive Animation by Conal Elliott and Paul Hudak.
Haskell 101
Coding, Observing, & Binding
Background: Key-Value Coding
Key-Value Coding (KVC) "itemPrice" ⇄ productModel.itemPrice
@interface Product : NSObject @property (strong) NSString *itemTitle; @property double itemPrice; @end !@implementation Product @end !Product *product = [[Product alloc] init]; ![product setItemPrice:10.0]; NSAssert([product itemPrice] == 10.0, @"price via setter"); !product.itemPrice += 5.0; NSAssert(product.itemPrice == 15.0, @"price via property"); ![product setValue:@25.0 forKey:@"itemPrice"]; NSAssert([[product valueForKey:@"itemPrice"] isEqual:@25.0], @"KVC");
Background: Key-Value Coding
Background: Key-Value Observing
Key-Value Coding (KVC) "itemPrice" ⇄ productModel.itemPrice
Key-Value Observing (KVO) - (void)itemPriceDidChange
@interface TraditionalObserver : NSObject @property (weak) Product *observee; @property int changeCount; @end !@implementation TraditionalObserver !- (id)initWithProduct:(Product *)product { if (self = [super init]) { self.observee = product; [product addObserver:self forKeyPath:@"itemTitle" options:0 context:NULL]; [product addObserver:self forKeyPath:@"itemPrice" options:0 context:NULL]; } return self; } !- (void)dealloc { [self.observee removeObserver:self forKeyPath:@"itemTitle"]; [self.observee removeObserver:self forKeyPath:@"itemPrice"]; } !- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSAssert(object == self.observee, @"unexpected observee!"); self.changeCount++; // remember how many changes take place if ([keyPath isEqualToString:@"itemTitle"]) [self titleDidChange]; if ([keyPath isEqualToString:@"itemPrice"]) [self priceDidChange]; } !@end
!@interface Product : NSObject @property (strong) NSString *itemTitle; @property double itemPrice; @end !@interface TraditionalObserver : NSObject @property (weak) Product *observee; @property int changeCount; @end !!Product *p = [[Product alloc] init]; TraditionalObserver *observer = [[TraditionalObserver alloc] initWithProduct:p]; NSAssert(observer.changeCount == 0, @"KVO change tracking"); !p.itemTitle = @"Paper"; p.itemPrice = 0.99; !NSAssert(observer.changeCount == 2, @"KVO change tracking");
Traditional Key-Value Observing
#import <ReactiveCocoa/ReactiveCocoa.h> @interface Product : NSObject @property (strong) NSString *itemTitle; @property double itemPrice; @end !@interface TraditionalObserver : NSObject @property (weak) Product *observee; @property int changeCount; @end !!Product *product = [[Product alloc] init]; !NSUInteger __block priceChangeCount = 0; [RACObserve(product, itemPrice) subscribeNext:^(NSNumber *newPrice) { priceChangeCount++; }]; !NSAssert(priceChangeCount == 1, @"reactive change tracking (initial)"); product.itemPrice = 9.99; NSAssert(priceChangeCount == 2, @"reactive change tracking (next value)");
Reactive Subscription
Product *product = [[Product alloc] init]; !NSUInteger __block priceChangeCount = 0; ![RACObserve(product, itemPrice) subscribeNext:^(NSNumber *newPrice) { priceChangeCount++; }]; !RACSignal *priceSignal = [product rac_valuesForKeyPath:@"itemPrice" observer:self]; [priceSignal subscribeNext:^(NSNumber *newPrice) { priceChangeCount++; }]; !NSAssert(priceChangeCount == 2, @"reactive change tracking"); product.itemPrice = 9.99; NSAssert(priceChangeCount == 4, @"reactive change tracking");
RACObserve Macro Expanded
Background: Cocoa Bindings
Key-Value Coding (KVC) "itemPrice" ⇄ productModel.itemPrice
Key-Value Observing (KVO) - (void)itemPriceDidChange
Cocoa Bindings priceField.text ⇄ productModel.price
@interface Product : NSObject @property (strong) NSString *itemTitle; @property double itemPrice; @end !@interface ProductViewController : NSViewController @property (strong) IBOutlet NSTextField *titleField; @end !Product *product = [[Product alloc] init]; !ProductViewController *controller = [[ProductViewController alloc] init]; NSTextField *field = controller.titleField; ![field bind:NSValueBinding toObject:product withKeyPath:@"itemTitle" options:nil]; !product.itemTitle = @"Paper"; NSAssert([field.stringValue isEqualToString:@"Paper"], @"good name"); !product.itemTitle = @"Paper++"; NSAssert([field.stringValue isEqualToString:@"Paper++"], @"great name!");
Cocoa Bindings
@interface Product : NSObject @property (strong) NSString *itemTitle; @property double itemPrice; @end !@interface ProductViewController : NSViewController @property (strong) IBOutlet NSTextField *titleField; @end !Product *product = [[Product alloc] init]; UITextField *textField = [[UITextField alloc] init]; !RACChannelTo(textField, text) = RACChannelTo(product, itemTitle); !product.itemTitle = @"Paper"; NSAssert([[textField text] isEqualToString:@"Paper"], @"model ⇢ view"); !textField.text = @"Flappy Paper"; NSAssert([product.itemTitle isEqualToString:@"Flappy Paper"], @"view ⇠ model");
ReactiveCocoa Bindings
Product *product = [[Product alloc] init]; UITextField *textField = [[UITextField alloc] init]; !RACChannelTo(textField, text) = RACChannelTo(product, itemTitle); !RACKVOChannel *viewChannel = [[RACKVOChannel alloc] initWithTarget:textField keyPath:@"text" nilValue:nil]; !RACKVOChannel *modelChannel = [[RACKVOChannel alloc] initWithTarget:product keyPath:@"itemTitle" nilValue:nil]; !viewChannel[@"followingTerminal"] = modelChannel[@"followingTerminal"]; !!!product.itemTitle = @"Paper"; NSAssert([[textField text] isEqualToString:@"Paper"], @"model ⇢ view"); !textField.text = @"Flappy Paper Tweet Saga++ by Fifty-Four Ltd."; NSAssert([product.itemTitle isEqualToString:@"Flappy Paper Tweet Saga++ by Fifty-Four Ltd."], @"view ⇠ model");
RACChannelTo Macro Expanded
Cocoa Bindings vs. ReactiveCocoa
• No iOS support
• No block support
• Cumbersome value transformation
• No signal coalescing
• Difficult to debug
• Interface Builder
User Interface & Commands
ReactiveCocoa UI mechanisms
• Categories & Blocks
Commands
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; ![button addTarget:self action:@selector(buttonPressed:) forControlEvents:UIControlEventTouchUpInside]; !- (IBAction)buttonPressed:(id)sender { // do something }
Commands
UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; !button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) { // do something return [RACSignal empty]; }];
UI Complexity Reduction
- (BOOL)isFormValid { return [self.usernameField.text length] > 0 && [self.emailField.text length] > 0 && [self.passwordField.text length] > 0 && [self.passwordField.text isEqual:self.passwordVerificationField.text]; } !#pragma mark - UITextFieldDelegate !- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { self.createButton.enabled = [self isFormValid]; return YES; }
Sample from http://nshipster.com/reactivecocoa/
Delegate Pattern:
UI Complexity Reduction
RACSignal *formValid = [RACSignal combineLatest:@[ self.username.rac_textSignal, self.emailField.rac_textSignal, self.passwordField.rac_textSignal, self.passwordVerificationField.rac_textSignal ] reduce:^(NSString *name, NSString *email, NSString *pw1, NSString *pw2) { return @(name.length && email.length && pw1.length > 8 && [pw1 isEqual:pw2]); }]; RAC(self.createButton.enabled) = formValid;
Sample from http://nshipster.com/reactivecocoa/
Reactive Pattern:
Model-View-ViewModel
Collections & Sequences
NSArray *values = @[ @"red", @"green", @"blue" ]; !NSArray *shortUppercaseColors = [[values filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(NSString *color, NSDictionary *bindings) { return [color length] <= 4; }]] valueForKeyPath:@"uppercaseString"]; !NSAssert(([shortUppercaseColors isEqualToArray:@[ @"RED", @"BLUE" ]]), @"");
NSArray Filter & Map
NSArray *values = @[ @"red", @"green", @"blue" ]; !RACSequence *seq = [[values.rac_sequence filter:^BOOL(NSString *color) { return [color length] <= 5; }] map:^id(NSString *color) { return [color uppercaseString]; }]; !NSAssert(([[seq array] isEqualToArray:@[ @"RED", @"BLUE" ]]), @"colors");
RACSequence Filter & Map
Sequences are evaluated lazily
ReactiveCocoa Class Hierarchy
RACStream
RACSignal
RACChannelTerminal
RACChannel
RACSubject
RACSequence
Resources
• https://github.com/ReactiveCocoa/ReactiveCocoa
• http://nshipster.com/reactivecocoa/
• http://www.teehanlax.com/blog/getting-started-with-reactivecocoa/
• http://cocoasamurai.blogspot.com/2013/03/basic-mvvm-with-reactivecocoa.html
• http://stackoverflow.com/questions/tagged/reactive-cocoa
• https://speakerdeck.com/joshaber/better-code-for-a-better-world