66
ReactiveCocoa and Swift Better Together @ColinEberhardt ShinobiControls

ReactiveCocoa and Swift, Better Together

Embed Size (px)

DESCRIPTION

ReactiveCocoa is an elegant framework that radically changes the way we structure our applications and handle flows of data. However, it's beauty is somewhat marred by Objective-C! In this talk Colin will cover the basics of ReactiveCocoa and the principles of Functional Reactive Programming. Through simple practical examples he will show how ReactiveCocoa and Swift form a beautiful partnership.

Citation preview

Page 1: ReactiveCocoa and Swift, Better Together

ReactiveCocoa and Swift Better Together

@ColinEberhardt ShinobiControls

Page 2: ReactiveCocoa and Swift, Better Together

www.shinobicontrols.com

Page 3: ReactiveCocoa and Swift, Better Together
Page 4: ReactiveCocoa and Swift, Better Together

What is ReactiveCocoa?

Page 5: ReactiveCocoa and Swift, Better Together
Page 6: ReactiveCocoa and Swift, Better Together

Functional Reactive Programming

Functional Programming

Reactive Programming+

=

Page 7: ReactiveCocoa and Swift, Better Together

ReactiveCocoa In my own words

Page 8: ReactiveCocoa and Swift, Better Together

Every line of code we write is executed in reaction to an event

Page 9: ReactiveCocoa and Swift, Better Together

But … these events come in many different forms

KVO

NSNotification

delegatestarget-action

callbacks

Page 10: ReactiveCocoa and Swift, Better Together

ReactiveCocoa provides a common interface for all events!

Page 11: ReactiveCocoa and Swift, Better Together

… this allows us to define a language for manipulating,

transforming and coordinating events

Page 12: ReactiveCocoa and Swift, Better Together

ReactiveCocoa Made Simple

Page 13: ReactiveCocoa and Swift, Better Together

Get Reactive

let textSignal: RACSignal = usernameTextField.rac_textSignal()

textSignal.subscribeNext { (text: AnyObject!) -> Void in let textString = text as String println(textString) }

Page 14: ReactiveCocoa and Swift, Better Together

Get Reactive

Page 15: ReactiveCocoa and Swift, Better Together

Objective-C Friction

let textSignal: RACSignal = usernameTextField.rac_textSignal()

textSignal.subscribeNext { (text: AnyObject!) -> Void in let textString = text as String println(textString) }

Page 16: ReactiveCocoa and Swift, Better Together

Simplified with Swift

textSignal.subscribeNextAs { (text: String) -> () in println(text) }

func subscribeNextAs<T>(nextClosure:(T) -> ()) -> () { self.subscribeNext { (next: AnyObject!) -> () in let nextAsT = next! as T nextClosure(nextAsT) } }

textSignal.subscribeNext { (text: AnyObject!) -> Void in let textString = text as String println(textString) }

Page 17: ReactiveCocoa and Swift, Better Together

Signals

• A signal emits events

• next

• error

• completed

• A signal can have none, one or more subscribers

textSignal.subscribeNextAs({ (text: String) in println(text) }, error: { (error) in // ... }, completed: { // ... })

Page 18: ReactiveCocoa and Swift, Better Together

Events

Signals can emit none, one or more next events, optionally followed by either an error or completed

NEXT NEXT NEXT NEXT …

NEXT NEXT ERROR

COMPLETEDintervals do not have to be regular!

Page 19: ReactiveCocoa and Swift, Better Together

Signal all things

• Network request

• A single next, followed by a completed

• Large download

• Multiple next events, representing partial data, followed by completed

• UI control

• An infinite stream of next events

Page 20: ReactiveCocoa and Swift, Better Together

filter

A filter is a ‘gate’, filtering-out events which do not match the given condition

let textSignal: RACSignal = usernameTextField.rac_textSignal()

let filteredText = textSignal.filterAs { (text: NSString) -> Bool in return text.length > 3 }

filteredText.subscribeNextAs { (text: String) in println(text) }

Page 21: ReactiveCocoa and Swift, Better Together
Page 22: ReactiveCocoa and Swift, Better Together

What exactly are events?

• What does a next event actually look like?

• Anything!

• Signals are an interface for handling asynchronous events

• The event contents are context dependant

Page 23: ReactiveCocoa and Swift, Better Together

map

Transforms each next event (please ignore the types!)

let textSignal: RACSignal = usernameTextField.rac_textSignal()

let textLength = textSignal.mapAs { (text: NSString) -> NSNumber in return text.length }

textLength.subscribeNextAs { (length: NSNumber) in println(length) }

Page 24: ReactiveCocoa and Swift, Better Together
Page 25: ReactiveCocoa and Swift, Better Together

Creating a pipelinelet textSignal: RACSignal = usernameTextField.rac_textSignal()

let textLength = textSignal.mapAs { (text: NSString) -> NSNumber in return text.length }

let filteredText = textLength.filterAs { (number: NSNumber) -> Bool in return number > 3 }

filteredText.subscribeNextAs { (length: NSNumber) in println(length) }

Page 26: ReactiveCocoa and Swift, Better Together

Fluent syntax

usernameTextField .rac_textSignal() .mapAs { (text: NSString) -> NSNumber in return text.length } .filterAs { (number: NSNumber) -> Bool in return number > 3 } .subscribeNextAs { (length: NSNumber) in println(length) }

Page 27: ReactiveCocoa and Swift, Better Together

rac_textSignal- map- filter- subscribeNext-string- number- number-

PUSH%

ReactiveCocoa Signals

Lazy Sequences

lazy% map% filter% generator%string% number% number%

PULL$

Page 28: ReactiveCocoa and Swift, Better Together

TweetSentiment

Page 29: ReactiveCocoa and Swift, Better Together
Page 30: ReactiveCocoa and Swift, Better Together

searchTextField .rac_textSignal() .mapAs { (text: NSString) -> UIColor in text.length <= 3 ? UIColor.lightRedColor() : UIColor.whiteColor() } .setKeyPath("backgroundColor", onObject: searchTextField)

Page 31: ReactiveCocoa and Swift, Better Together

Don’t search for very short search terms

Page 32: ReactiveCocoa and Swift, Better Together

Don’t issue a new query on each key press

Page 33: ReactiveCocoa and Swift, Better Together

searchTextField .rac_textSignal() .throttle(0.5) .filterAs { (text: NSString) -> Bool in text.length > 3 } .subscribeNextAs { (text: NSString) -> () in println(text) }

Page 34: ReactiveCocoa and Swift, Better Together

rac_textSignal-

filter- setKeyPath-

thro5le- filter- subscribeNext-

Page 35: ReactiveCocoa and Swift, Better Together

Creating Signals

Page 36: ReactiveCocoa and Swift, Better Together

private func signalForSearchWithText(text: String) -> RACSignal {

func requestforSearchText(text: String) -> SLRequest { return ...

} return RACSignal.createSignal { subscriber -> RACDisposable! in let request = requestforSearchText(text) let maybeTwitterAccount = self.getTwitterAccount() if let twitterAccount = maybeTwitterAccount { request.account = twitterAccount request.performRequestWithHandler { (data, response, _) -> Void in if response != nil && response.statusCode == 200 { let timelineData = NSJSONSerialization.parseJSONToDictionary(data) subscriber.sendNext(timelineData) subscriber.sendCompleted() } else { subscriber.sendError(TwitterInstantError.InvalidResponse.toError()) } } } else { subscriber.sendError(TwitterInstantError.NoTwitterAccounts.toError()) } return nil } }

Page 37: ReactiveCocoa and Swift, Better Together

searchTextField .rac_textSignal() .throttle(0.5) .filterAs { (text: NSString) -> Bool in text.length > 3 } .mapAs { (text: NSString) -> RACStream in self.signalForSearchWithText(text) } .subscribeNextAs { ... }

Page 38: ReactiveCocoa and Swift, Better Together

searchTextField .rac_textSignal() .throttle(0.5) .filterAs { (text: NSString) -> Bool in text.length > 3 } .flattenMapAs { (text: NSString) -> RACStream in self.signalForSearchWithText(text) } .subscribeNextAs { ... }

Page 39: ReactiveCocoa and Swift, Better Together

searchTextField .rac_textSignal() .throttle(0.5) .filterAs { (text: NSString) -> Bool in text.length > 3 } .flattenMapAs { (text: NSString) -> RACStream in self.signalForSearchWithText(text) } .deliverOn(RACScheduler.mainThreadScheduler()) .subscribeNextAs { (tweets: NSDictionary) in let statuses = tweets["statuses"] as [NSDictionary] self.tweets = statuses.map { Tweet(json: $0) } self.tweetsTableView.reloadData() self.tweetsTableView.scrollToTop() }

Page 40: ReactiveCocoa and Swift, Better Together

rac_textSignal-

filter- setKeyPath-

thro5le- filter- fla5enMap-

twi5erSearch-

deliverOn- subscribeNext-

Page 41: ReactiveCocoa and Swift, Better Together

requestAccessToTwitterSignal() .then { self.searchTextField.rac_textSignal() } .filterAs { ... } .throttle(0.5) ...

Page 42: ReactiveCocoa and Swift, Better Together

rac_textSignal-

filter- setKeyPath-

thro5le- filter- fla5enMap-

twi5erSearch-

deliverOn- subscribeNext-

requestAccess-signal- then-

Page 43: ReactiveCocoa and Swift, Better Together

Error Handling

Page 44: ReactiveCocoa and Swift, Better Together

requestAccessToTwitterSignal() .then { self.searchTextField.rac_textSignal() } .filterAs { ... } .throttle(0.5) .doNext { ... } .flattenMapAs { ... self.signalForSearchWithText(text) } .deliverOn(RACScheduler.mainThreadScheduler()) .subscribeNextAs({ ... }, { (error) in println(error) })

Page 45: ReactiveCocoa and Swift, Better Together

Fetching Sentiment Data

Page 46: ReactiveCocoa and Swift, Better Together

Fire a sentiment API request for each tweet and merge the signals?

Page 47: ReactiveCocoa and Swift, Better Together

Fetch sentiment data when cells become visible?

Page 48: ReactiveCocoa and Swift, Better Together

Fetch sentiment data when a cell has been visible for a short duration?

Page 49: ReactiveCocoa and Swift, Better Together

Signal All Things!

Page 50: ReactiveCocoa and Swift, Better Together

RACSignal .interval(0.5, onScheduler: RACScheduler(priority: RACSchedulerPriorityBackground)) .take(1) .takeUntil(rac_prepareForReuseSignal) .flattenMap { (next) -> RACStream in self.obtainSentimentSignal(hasTweet) } .deliverOn(RACScheduler.mainThreadScheduler()) .subscribeNextAs { (sentiment: String) in NSNotificationCenter.defaultCenter() .postNotificationName("sentiment", object: sentiment) self.sentimentIndicator.backgroundColor = self.sentimentToColor(sentiment) }

Page 51: ReactiveCocoa and Swift, Better Together
Page 52: ReactiveCocoa and Swift, Better Together

I curried a function!

Page 53: ReactiveCocoa and Swift, Better Together
Page 54: ReactiveCocoa and Swift, Better Together

func scale(domainMin: Double, domainMax: Double, screenMin: Double, screenMax: Double, pt: Double) -> CGFloat {

let value = ((pt - domainMin) / (domainMax - domainMin)) * (screenMax - screenMin) + screenMin return CGFloat(value) }

Page 55: ReactiveCocoa and Swift, Better Together

func scale(domainMin: Double, domainMax: Double, screenMin: Double, screenMax: Double)(pt: Double) -> CGFloat {

let value = ((pt - domainMin) / (domainMax - domainMin)) * (screenMax - screenMin) + screenMin return CGFloat(value) }

Page 56: ReactiveCocoa and Swift, Better Together

let xscale = scale(0, Double(maxValue), 35, Double(self.bounds.width - 35)) let yscale = scale(0, 3, 0, Double(self.bounds.height))

positiveLayer.frame = CGRect(x: xscale(pt: 0), y: yscale(pt: 1), width: xdelta(pt1: Double(positive), pt2: 0.0), height: yscale(pt: 1)) neutralLayer.frame = ...

func delta(scale:(Double) -> CGFloat)(pt1: Double, pt2: Double) -> CGFloat { return scale(pt1) - scale(pt2) }

let xdelta = delta(xscale)

Page 57: ReactiveCocoa and Swift, Better Together
Page 58: ReactiveCocoa and Swift, Better Together

Better Together

Page 59: ReactiveCocoa and Swift, Better Together

Swift loves Fluent APIs

Page 60: ReactiveCocoa and Swift, Better Together

requestAccessToTwitterSignal() .then { self.searchTextField.rac_textSignal() } .filterAs { (text: NSString) -> Bool in text.length > 3 } .doNext { (any) in self.tweetsTableView.alpha = 0.5 } .throttle(0.5) .doNext { (any) in NSNotificationCenter.defaultCenter().postNotificationName("sentiment", object: "reset") } .flattenMapAs { (text: NSString) -> RACStream in self.signalForSearchWithText(text) } .deliverOn(RACScheduler.mainThreadScheduler()) .subscribeNextAs({ (tweets: NSDictionary) in let statuses = tweets["statuses"] as [NSDictionary] self.tweets = statuses.map { Tweet(json: $0) } self.tweetsTableView.reloadData() self.tweetsTableView.scrollToTop() self.tweetsTableView.alpha = 1.0 }, { (error) in println(error) })

Page 61: ReactiveCocoa and Swift, Better Together

http://stackoverflow.com/questions/3124001/fluent-interface-pattern-in-objective-c

Page 62: ReactiveCocoa and Swift, Better Together

http://stackoverflow.com/questions/3124001/fluent-interface-pattern-in-objective-c

Page 63: ReactiveCocoa and Swift, Better Together

[[[[[[[[self requestAccessToTwitterSignal] then:^RACSignal *{ @strongify(self) return self.searchText.rac_textSignal; }] filter:^BOOL(NSString *text) { @strongify(self) return [self isValidSearchText:text]; }] throttle:0.5] flattenMap:^RACStream *(NSString *text) { @strongify(self) return [self signalForSearchWithText:text]; }] map:^id(NSDictionary *jsonSearchResult) { NSArray *statuses = jsonSearchResult[@"statuses"]; NSArray *tweets = [statuses linq_select:^id(id tweet) { return [RWTweet tweetWithStatus:tweet]; }]; return tweets; }] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSArray *tweets) { [self.resultsViewController displayTweets:tweets]; } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }];

Page 64: ReactiveCocoa and Swift, Better Together

ReactiveCocoa hates state

Page 65: ReactiveCocoa and Swift, Better Together

39 constants12 variables

6 outlets (not my fault!)

1 UIWindow5 UI state variables

Page 66: ReactiveCocoa and Swift, Better Together

ReactiveCocoa and Swift Better Together

@ColinEberhardt ShinobiControls

TwitterSentiment:https://github.com/ColinEberhardt/ReactiveSwiftLondon

MVVM with ReactiveCocoa and Swift:https://github.com/ColinEberhardt/ReactiveSwiftFlickrSearch

Tutorials:http://www.raywenderlich.com/u/ColinEberhardt