intro to reactivecocoa

Post on 16-Apr-2017

840 Views

Category:

Engineering

0 Downloads

Preview:

Click to see full reader

TRANSCRIPT

Getting started with ReactiveCocoaKyle LeNeau

Director and Lead Architect, Mobile @ SmartThings

About Me

• Kyle LeNeau

• Director and Lead Architect, Mobile @ SmartThings

• @kleneau

• http://github.com/KyleLeNeau

• kyle@smartthings.com

Simple way for someone to turn their Home into a SmartHome. Open Platform for consumers and developers.

Why SmartThings Adopted ReactiveCocoa

• SmartThings (IoT) is, by definition, very reactive. Users need a way to know that their light turned when a SmartApp told it to do so.

• SmartThings platform is highly asynchronous. There is an event stream that feeds the app data while foregrounded.

• Cleaner API’s that allow more functional programming.

• Ability to start slowly adopting the Model View View Model pattern (MVVM).

• Ability to chain functional operations together to produce clean code and better error handling.

https://github.com/ReactiveCocoa/ReactiveCocoa

#import <ReactiveCocoa/ReactiveCocoa.h>

What is ReactiveCocoaSignal Represent data that will be delivered in the future. Users must subscribe to them to receive values.

Subscription Anything that is waiting for, or capable of, waiting for events from signals. (next, error, completed).

SchedulersSerial execution queue for signals to perform work or deliver results on. Similar to Grand Central Dispatch queues.

Subjects A manually controlled signal, “mutable” variant of a signal. Very useful to bridge non-RAC code into the world of signals (sendNext, sendError, sendCompleted).

DisposablesUsed for cancellation of signals and resource cleanup. When a subscription is disposed of, the corresponding subscriber will not receive any further events for the signal.

What is a SignalSignals generally represent data that will be delivered in the future. As work is performed or data is received, values are sent on the signal, which pushes them out to any subscribers. Users must subscribe to a signal in order to access its values.@property (nonatomic, strong) NSString *userName;

[RACObserve(self, userName) subscribeNext:^(NSString *newName) { NSLog(@"User name entered: %@", newName); }];

self.userName = "Kyle"; // Outputs // User name entered: Kyle

self.userName = "Joe"; // Outputs // User name entered: Joe

Signal Events• next - A signal can send any number of next events

before it terminates after an error of completes.

• completed - Marks the end of a signal, usually in a successful situation. The subscription removes itself automatically.

• error - Marks the end of a signal, usually in a bad state. The subscription removes itself automatically.

• interrupted - Happens when a signal is forcibly stopped. Introduced for the Swift API in RAC 3.0.

1 2next next completed

1 2next next error

error

completed

Basic Operators

• doNext, doCompleted, doError - Inject side effects to a signal without subscribing to it

• map - transform the value of a signal to something else

• filter - filter the signal values based on condition

• flattenMap - transform one signal into another

• merge - combine the results of many signals into one

• takeUntil - take all the signal values until another signal tells it to stop

Login Form Example@property (nonatomic, strong) NSString *userName; @property (nonatomic, strong) NSString *password; @property (nonatomic, strong) UIButton *loginButton;

- (void)viewDidLoad { [super viewDidLoad]; [RACObserve(self, userName) subscribeNext:^(NSString *newName) { NSLog(@"User name entered: %@", newName); }]; [RACObserve(self, password) subscribeNext:^(NSString *newPassword) { NSLog(@"Password entered: %@", newPassword); }];

}

// loginButton is Disabled // Somewhere: self.userName = @"Kyle";

// Output: > User name entered: Kyle > Form is Valid: false

// loginButton is Disabled

// Somewhere: self.password = @"SmartThings";

// Output: > Password entered: SmartThings > Form is Valid: true // loginButton is Enabled

NSArray *signals = @[RACObserve(self, userName), RACObserve(self, password)]; RACSignal *validSignal = [RACSignal combineLatest:signals reduce:^(NSString *userName, NSString *password) { // return YES or NO for valid fields return @(userName != nil && userName.length > 0 && password != nil && password.length > 0); }]; [validSignal subscribeNext:^(NSNumber *isValid) { NSLog(@"Form is Valid: %@", isValid.boolValue ? @"true" : @"false"); }];

RAC(self.loginButton, enabled) = validSignal;

UI Integration@property (nonatomic, strong) UITextField *tweetText;

- (void)viewDidLoad { [super viewDidLoad]; [self.tweetText.rac_textSignal subscribeNext:^(NSString *newTweetText) { NSLog(@"New Tweet: %@", newTweetText); }];

[[self.tweetText.rac_textSignal map:^id(NSString *newTweetText) { return @(140 - newTweetText.length); }] subscribeNext:^(NSNumber *length) { NSLog(@"Characters Remaining: %@", length); }];

self.tweetText.text = @"Hello Midwest Mobile Summit"; // > New Tweet: Hello Midwest Mobile Summit // > Characters Remaining: 113 }

ReactiveCocoa UITextField Category@implementation UITextField (RACSignalSupport)

- (RACSignal *)rac_textSignal { @weakify(self); return [[[[RACSignal defer:^{ @strongify(self); return [RACSignal return:self]; }] concat:[self rac_signalForControlEvents:UIControlEventAllEditingEvents]] map:^(UITextField *x) { return x.text; }] takeUntil:self.rac_willDeallocSignal]; }

@end

Working with Legacy Async API’s- (void)loginWithUser:(NSString *)username password:(NSString *)password success:(void(^)(NSArray *))success error:(void(^)(NSError *))error { // ... }

- (void)loadCachedTweets:(void(^)(NSArray *))success error:(void(^)(NSError *))error { // ... }

- (void)fetchTweetsAfterTweet:(NSObject *)tweet success:(void(^)(NSArray *))success error:(void(^)(NSError *))error { // ... }

Ugly Nested Blocks[self loginWithUser:@"Kyle" password:@"foo" success:^(NSArray *tweet) { [self loadCachedTweets:^(NSArray *cachedTweets) { [self.allTweets addObjectsFromArray:cachedTweets]; [self fetchTweetsAfterTweet:cachedTweets.lastObject success:^(NSArray *newTweets) { [self.allTweets addObjectsFromArray:newTweets]; } error:^(NSError *fetchError) { // ... }]; } error:^(NSError *cacheError) { // ... }]; } error:^(NSError *loginError) { // ... }];

Creating a RACSignal- (RACSignal *)loginWithUser:(NSString *)username password:(NSString *)password { return [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) { [self loginWithUser:username password:password success:^(User *user) { [subscriber sendNext:user]; [subscriber sendCompleted]; } error:^(NSError *error) { [subscriber sendError:error]; }]; // no way to cancel this so return nil return nil; }]; }

[[[self loginWithUser:@"Kyle" password:@"pass"] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id *user) { NSLog(@"logged in, yay!"); self.label.text = @"Welcome!"; } error:^(NSError *error) { //... }];

Composing many RACSignals into oneRACSignal *tweetsSignal = [[[self loginWithUser:@"Kyle" password:@"pass"] flattenMap:^RACStream *(id value) { return [[self loadCachedTweets] collect]; }] flattenMap:^RACStream *(NSArray *cachedTweets) { return [[self fetchTweetsAfterTweets:cachedTweets] collect]; }];

[tweetsSignal subscribeNext:^(NSArray *allTweets) { // all Tweets are loaded } error:^(NSError *error) { // something threw an error // hide activity indicator } completed:^{ // everything is done // hide activity indicator }];

@property (nonatomic, strong) NSMutableArray *allTweets; RAC(self, allTweets) = tweetsSignal;

Another ReactiveCocoa Category@implementation NSNotificationCenter (RACSupport)

- (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object { @unsafeify(object); return [RACSignal createSignal:^(id<RACSubscriber> subscriber) { @strongify(object); id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) { [subscriber sendNext:note]; }]; return [RACDisposable disposableWithBlock:^{ [self removeObserver:observer]; }]; }]; }

@end

Hot vs Cold Signal

• A “hot signal” is a signal that sends values (does work) regardless of whether it has any subscribers.

• A “cold signal” is a signal that defers its work and sending of values until it has a subscriber. A “cold signal” will perform it’s work and send values for each subscriber.

• In Objective-C the RACSignal type represents both hot and cold. It also has to be made hot with a ReplaySubject (-replay, -replayLast, & -replayLazily)

Whats New in 3.0

• Strongly typed signals and producers with Swift

• Clearer intent -> Signal (Hot Signal) & SignalProducer (Cold Signal)

• Clearer property types for Swift (lack of KVC)

• Actions -> replacement for Commands

• Custom operators for Swift

SignalProducers

func login() -> SignalProducer<User, NoError> { return SignalProducer { sink, disposable in // Do network logic here and then sendNext, sendCompleted or sendError // sendNext(sink, user) // sendError(NSError()) sendCompleted(sink) } }

PropertyOfpublic class LoginViewModel { public let userName = MutableProperty<String?>(nil) public let password = MutableProperty<String?>(nil) public lazy var loginEnabled: PropertyOf<Bool> = { let property = MutableProperty(false) property <~ combineLatest(self.userName.producer, self.password.producer) |> map { userName, password in if let userName = userName, password = password { return true } else { return false } } return PropertyOf(property) }() }

ActionsActions take in input and to do some work to produce outputs and/or errors. Actions enforce serial execution and will block multiple calls while executing.

public lazy var loginAction: Action<(String, String), User, NSError> = { return Action(enabledIf: self.loginEnabled, { username, password in return self.login(username, password: password) }) }()

private func login(username: String, password: String) -> SignalProducer<User, NSError> { return SignalProducer { sink, disposable in sendCompleted(sink) } } self.loginAction.apply("Kyle", "Password")

Why would you use ReactiveCoca?

• Increase readability of your code for yourself and other developers.

• Express intent through functional programming.

• Take advantage of a ton of functional operators.

• Avoid the block indentation mess, stay left aligned.

• Reduce the amount of state you need to hold when dealing with delegate methods.

How SmartThings Adopted ReactiveCocoa

• Start small to get a feel for the patterns. We started by making REST api calls signal based rather than method block based.

• Slow refactoring, as we had time and focus on an area of the code base. Had to keep shipping software.

• New development and side classes (version 1, version 2) were written with ReactiveCocoa as we got comfortable.

• New view controllers and development started with MVVM to slim the view controller logic. MVVM allowed us to test our view model logic.

• Moved NSNotification listening and filtering to ReactiveCocoa.

Other Ways SmartThings Uses ReactiveCocoa

The event stream sends NSNotifications which trigger tiles to update in real time.

SmartThings Hub & Platform

Motion!

Lessons Learned

• Start slow, it can take a while to get your team on board and up to speed. There is a learning curve.

• Threading - Make sure you are delivering to the right thread and are capturing self accordingly @strongify(self), @weakify(self).

• Knowing when to use a Hot Signal (rare)

• Make sure you understand the subscription events.

More Information• https://github.com/ReactiveCocoa/ReactiveCocoa

• http://nshipster.com/reactivecocoa/

• http://www.raywenderlich.com/62699/reactivecocoa-tutorial-pt1 (2 part series)

• http://www.objc.io/issue-13/mvvm.html

• http://www.objc.io/books/ - Functional Programming with Swift

• https://leanpub.com/iosfrp - Functional Reactive Programming on iOS using ReactiveCocoa by Ash Furrow

• https://github.com/octokit/octokit.objc - Github API Framework using AFNetworking and ReactiveCocoa

SmartThings is HiringiOS

Android Windows Firmware

Cloud (Groovy, Cassandra, Grails) QA

Test Engineers

Questions?

• Kyle LeNeau

• Director and Lead Architect, Mobile @ SmartThings

• @kleneau

• http://github.com/KyleLeNeau

• kyle@smartthings.com

top related