reactive cocoa made simple with swift
DESCRIPTION
A talk I delivered at iOSDevUK14 where I mixed ReactiveCocoa with Swift.TRANSCRIPT
ReactiveCocoa made Simple with Swift!
@ColinEberhardt ShinobiControls
Tutorials
What is ReactiveCocoa?
• ReactiveCocoa is an open-source framework created by the GitHub team
• Inspired by Functional Reactive Programming (FRP)
• Changes the way in which we structure our applications
• Inspired by Microsoft’s ReactiveExtensions (Rx)
Functional Reactive Programming
• Functional Programming
• First-class functions, passed as arguments to other functions
• Reactive Programming
• Focusses on data flow
• Functional Programming + Reactive Programming = FRP
FRP - WTF?
http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming
In my own words …
• Every line of code we write is executed in reaction to an event
• But … these events all have different interfaces
• delegates, target-action, KVO, closures
• ReactiveCocoa provides a standard interface for all events
• Allows us to define a higher level language
• Easier to test too!
Learn through play
Code Build Run
Explain! Repeat
Get Reactive
let textSignal: RACSignal = usernameTextField.rac_textSignal() !textSignal.subscribeNext { (text: AnyObject!) -> Void in let textString = text as String println(textString) }
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) } }
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: { // ... })
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!
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
Operations
• ReactiveCocoa allows you to perform operations on signals
• map, filter, skip, take, throttle …
• These operations are entirely agnostic to the source of the signal
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) }
What exactly are events?
• What does a next event actually look like?
• Anything!
• Signals are an interface for handling asynchronous events
• The event contents is context dependant
map
Transforms each next event
let textSignal: RACSignal = usernameTextField.rac_textSignal() !let textLength = textSignal.mapAs { (text: NSString) -> NSNumber in return text.length } !textLength.subscribeNextAs { (length: NSNumber) in println(length) }
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) }
Fluent syntax
Operations return RACSignal, allowing method chaining
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) }
rac_textSIgnal- filter- subscribeNext-
Value->-3-
map-
NSString- NSNumber-
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) }
Valid text fields
• Unfortunately we need to ‘box’ bool values.
• I am sure ReactiveSwift will fix this ;-)
let validUsernameSignal = usernameTextField.rac_textSignal() .mapAs { (text: NSString) -> NSNumber in return self.isValidUsername(text) } !validUsernameSignal.mapAs { (valid: NSNumber) -> UIColor in return valid.boolValue ? UIColor.clearColor() : UIColor.yellowColor() }.setKeyPath("backgroundColor", onObject: usernameTextField)
More Swift Magic!
!!RAC(usernameTextField, "backgroundColor") << validUsernameSignal.mapAs { (valid: NSNumber) -> UIColor in return valid.boolValue ? UIColor.clearColor() : UIColor.yellowColor() }
http://napora.org/a-swift-reaction/
More Sweet Sweet Swift!
func validToBackground(valid: NSNumber) -> UIColor { return valid.boolValue ? UIColor.clearColor() : UIColor.yellowColor() } !func isValidText(validator:(String) -> Bool)(text: NSString) -> NSNumber { return validator(text) } !let validUsernameSignal = usernameTextField.rac_textSignal() .mapAs(isValidText(isValidUsername)) .distinctUntilChanged() !RAC(usernameTextField, "backgroundColor") << validUsernameSignal.mapAs(validToBackground)
Login button enabled state
Combines two signals into a new one
let signUpActiveSignal = RACSignalEx.combineLatestAs( [validUsernameSignal, validPasswordSignal]) { (validUsername: NSNumber, validPassword: NSNumber) -> NSNumber in return validUsername && validPassword } !signUpActiveSignal.subscribeNextAs { (active: NSNumber) in self.signInButton.enabled = active }
The pipeline
rac_textSIgnal- map- backgroundColor-map-BOOL-NSString- UIColor-
password-
rac_textSIgnal- map- backgroundColor-map-BOOL-NSString- UIColor-
username-
combineLatest:reduce- subscribeNext-BOOL-
Reactive Button Click
Replaces target-action with a signal
signInButton.rac_signalForControlEvents(.TouchUpInside) .subscribeNext { (button) in println("clicked") }
Creating Signals
func signInSignal() -> RACSignal { return RACSignal.createSignal { (subscriber) -> RACDisposable! in println("Sign-in initiated") self.signInService.signInWithUsername(self.usernameTextField.text, password: self.passwordTextField.text) { (success) in println("Sign-in completed") subscriber.sendNext(success) subscriber.sendCompleted() } return nil } }
Using the sign-in signal
signInButton.rac_signalForControlEvents(.TouchUpInside) .map { (any) -> RACSignal in self.signInSignal() }.subscribeNext { (any) in println(any) }
Doing it right!
flattenMap subscribes to a signal, returning the resultant events
signInButton.rac_signalForControlEvents(.TouchUpInside) .flattenMap { (any) -> RACSignal in self.signInSignal() }.subscribeNext { (any) in println(any) }
Side-effects
Side-effects receive next events, but cannot mutate them
signInButton.rac_signalForControlEvents(.TouchUpInside) .doNext { (any) in self.signInButton.enabled = false; }.flattenMap { (any) -> RACSignal in self.signInSignal() }.subscribeNextAs { (success: NSNumber) in self.signInButton.enabled = true; self.handleSignInResult(success.boolValue) }
Login pipeline
rac_signalForControlEvents11 fla3enMap1 subscribeNext1UIBu3on1 BOOL1
doNext1signInSignal1
BOOL1
ReactiveCocoa Made Simple
• A signal emits events
• next
• error
• completed
• A signal can have multiple subscribers
• Signals can emit multiple next events, optionally followed by either an error or completed
Random cool stuff!
[[[[[[[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]; }] deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(NSDictionary *jsonSearchResult) { NSArray *statuses = jsonSearchResult[@"statuses"]; NSArray *tweets = [statuses linq_select:^id(id tweet) { return [RWTweet tweetWithStatus:tweet]; }]; [self.resultsViewController displayTweets:tweets]; } error:^(NSError *error) { NSLog(@"An error occurred: %@", error); }];
https://github.com/ColinEberhardt/ReactiveSwiftFlickrSearch
// a signal that emits events when visibility changes let visibleStateChanged = RACObserve(self, "isVisible").skip(1) !// filtered into visible and hidden signals let visibleSignal = visibleStateChanged.filter { $0.boolValue } let hiddenSignal = visibleStateChanged.filter { !$0.boolValue } !// a signal that emits when an item has been visible for 1 second let fetchMetadata = visibleSignal.delay(1).takeUntil(hiddenSignal) !fetchMetadata.subscribeNext { (next: AnyObject!) -> () in // fetch data }
Resources• https://github.com/ReactiveCocoa/ReactiveCocoa
• Read the Change Logs!
• My Stuff …
• http://www.raywenderlich.com/u/ColinEberhardt
• https://github.com/ColinEberhardt
• The code from this presentation
• https://github.com/ColinEberhardt/SwiftReactivePlayground
ReactiveCocoa made Simple with Swift!
@ColinEberhardt ShinobiControls