intro to reactivecocoa

29
Getting started with ReactiveCocoa Kyle LeNeau Director and Lead Architect, Mobile @ SmartThings

Upload: kleneau

Post on 16-Apr-2017

839 views

Category:

Engineering


0 download

TRANSCRIPT

Page 1: Intro to ReactiveCocoa

Getting started with ReactiveCocoaKyle LeNeau

Director and Lead Architect, Mobile @ SmartThings

Page 2: Intro to ReactiveCocoa

About Me

• Kyle LeNeau

• Director and Lead Architect, Mobile @ SmartThings

• @kleneau

• http://github.com/KyleLeNeau

[email protected]

Page 3: Intro to ReactiveCocoa

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

Page 4: Intro to ReactiveCocoa

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.

Page 5: Intro to ReactiveCocoa

https://github.com/ReactiveCocoa/ReactiveCocoa

#import <ReactiveCocoa/ReactiveCocoa.h>

Page 6: Intro to ReactiveCocoa

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.

Page 7: Intro to ReactiveCocoa

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

Page 8: Intro to ReactiveCocoa

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

Page 9: Intro to ReactiveCocoa

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

Page 10: Intro to ReactiveCocoa

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;

Page 11: Intro to ReactiveCocoa

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 }

Page 12: Intro to ReactiveCocoa

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

Page 13: Intro to ReactiveCocoa

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 { // ... }

Page 14: Intro to ReactiveCocoa

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) { // ... }];

Page 15: Intro to ReactiveCocoa

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) { //... }];

Page 16: Intro to ReactiveCocoa

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;

Page 17: Intro to ReactiveCocoa

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

Page 18: Intro to ReactiveCocoa

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)

Page 19: Intro to ReactiveCocoa

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

Page 20: Intro to ReactiveCocoa

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) } }

Page 21: Intro to ReactiveCocoa

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) }() }

Page 22: Intro to ReactiveCocoa

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")

Page 23: Intro to ReactiveCocoa

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.

Page 24: Intro to ReactiveCocoa

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.

Page 25: Intro to ReactiveCocoa

Other Ways SmartThings Uses ReactiveCocoa

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

SmartThings Hub & Platform

Motion!

Page 26: Intro to ReactiveCocoa

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.

Page 27: Intro to ReactiveCocoa

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

Page 28: Intro to ReactiveCocoa

SmartThings is HiringiOS

Android Windows Firmware

Cloud (Groovy, Cassandra, Grails) QA

Test Engineers

Page 29: Intro to ReactiveCocoa

Questions?

• Kyle LeNeau

• Director and Lead Architect, Mobile @ SmartThings

• @kleneau

• http://github.com/KyleLeNeau

[email protected]