reactivecocoa workshop
TRANSCRIPT
Building iOS apps with ReactiveCocoa
About me
Eliasz SawickiBlog: http://eluss.github.io/Twitter: @EliSawic
Let's begin
ReactiveCocoa
Functional Reactive Programming
WikipediaFunctional reactive programming (FRP) is a programming paradigm for reactive programming (asynchronous dataflow programming) using the building blocks of functional programming (e.g. map, reduce, filter).
Reactive Programming
Reactive Programming• Working with asynchronous dataflow
• Reacting to state changes
Functional Programming
Functional Programming• Immutable
assert(f(x) == f(x))
A personclass Person { var name: String init(name: String) { self.name = name }}
Mutablefunc personWithReversedName(person: Person) -> Person { person.name = String(person.name.characters.reverse()) return person}
let person = Person(name: "John")let reversedA = personWithReversedName(person)print(reversedA.name) // nhoJlet reversedB = personWithReversedName(person)print(reversedB.name) // John
Immutablefunc personWithReversedName(person: Person) -> Person { let name = String(person.name.characters.reverse()) let newPerson = Person(name: name) return newPerson}
let person = Person(name: "John")let reversedA = personWithReversedName(person)print(reversedA.name) // nhoJlet reversedB = personWithReversedName(person)print(reversedB.name) // nhoJ
Functional Programming• Immutable• Stateless
Statefulvar value = 0
func increment() { value += 1}
Statelessfunc increment(value: Int) -> Int { return value + 1}
Declarative
Imperative vs
Declarative
Imperativelet array = [0, 1, 2, 3, 4, 5]var evenNumbers = [Int]()for element in array { if element % 2 == 0 { evenNumbers.append(element) }}
Declarativelet array = [0, 1, 2, 3, 4, 5]let evenNumbers = array.filter { $0 % 2 == 0 }
Back to ReactiveCocoa
Event streams
Event Stream
Event
Non-Terminating
• Next
Terminating
• Completed
• Failed
• Interrupted
Observer
Signal
What is it?• Represents events over time
• Must be observed in order to access it's events
• Observing a signal does not trigger any side effects (push based)
• No random access to events
Signal's lifetime• Passes any number of Next events
• "Dies" when terminating event arrives
• Any new observer will receive Interrupted event
Observingsignal.observe { (event) in print(event)}signal.observeNext { (value) in print(value)}
signal.observeCompleter { print("Completed")}
Creating Signals
Basic signalSignal<String, NSError> { (observer) -> Disposable? in observer.sendNext("test") observer.sendCompleted() return ActionDisposable(action: { print("Signal disposed") })}
Pipe
let (signal, observer) = Signal<String, NoError>.pipe()
signal.observeNext({ text in print(text)})
signal.observeCompleted({ print("Test completed")})
observer.sendNext("It's a test") // It's a testobserver.sendCompleted() // Test completed
SignalProducer
What is it?• Represents tasks
• Creates signals
• Performs side effects
• Does not start it's work if not started
Injecting side effectslet producer = signalProducer .on(started: { print("Started") }, event: { event in print("Event: \(event)") }, failed: { error in print("Failed: \(error)") }, completed: { print("Completed") }, interrupted: { print("Interrupted") }, terminated: { print("Terminated") }, disposed: { print("Disposed") }, next: { value in print("Next: \(value)") })
Creating Signal Producers
Basic Signal ProducerSignalProducer<String, NSError> { (observer, composite) in composite.addDisposable({ print("Clearing work") })
observer.sendNext("In Progres...") observer.sendCompleted()}
Bufferlet (producer, observer) = SignalProducer<String, NoError>.buffer(3)observer.sendNext("test")observer.sendCompleted()
producer.startWithSignal { (signal, disposable) in signal.observeNext({ (text) in print(text) // test }) signal.observeCompleted({ print("Test completed") // Test completed })}observer.sendNext("is interrupted")observer.sendInterrupted()producer.startWithSignal { (signal, disposable) in signal.observeNext({ (text) in print(text) // test, is interrupted })
signal.observeInterrupted({ print("Test interrupted") // Test interrupted })}
Manipulating signals
Map
Maplet (numberSignal, observer) = Signal<Int, NoError>.pipe()let textSignal = numberSignal.map { (number) -> String in return "Number is \(number)"}numberSignal.observeNext { (number) in print(number) // 5}textSignal.observeNext { (text) in print(text) // Number is 5}observer.sendNext(5)
Filter
Filterlet (numberSignal, observer) = Signal<Int, NoError>.pipe()let fiveSignal = numberSignal.filter { (number) -> Bool in return number == 5}numberSignal.observeNext { (number) in print(number) // 6, 5}fiveSignal.observeNext { (number) in print(number) // 5}observer.sendNext(6)observer.sendNext(5)
Aggregating
Aggregatinglet (numberSignal, observer) = Signal<Int, NoError>.pipe()let aggregtingSignal = numberSignal.reduce(0) { (currentValue, addedValue) -> Int in return currentValue + addedValue}numberSignal.observeNext { (number) in print(number) // 5, 6}aggregtingSignal.observeNext { (number) in print("Aggregated \(number)") // Aggregated 11}observer.sendNext(5)observer.sendNext(6)observer.sendCompleted()
Skip repeats
Skip repeatslet (numberSignal, observer) = Signal<Int, NoError>.pipe()numberSignal.observeNext { (number) in print(number) // 1, 2, 2, 3}numberSignal.skipRepeats().observeNext { (number) in print(number) // 1, 2, 3}observer.sendNext(1)observer.sendNext(2)observer.sendNext(2)observer.sendNext(3)
Skip until
Skip untillet (numberSignal, observer) = Signal<Int, NoError>.pipe()numberSignal.observeNext { (number) in print(number) // 5, 6}let (trigger, triggerObserver) = Signal<Void, NoError>.pipe()numberSignal.skipUntil(trigger).observeNext { (number) in print("Triggered \(number)") // Triggered 6}observer.sendNext(5)triggerObserver.sendNext()observer.sendNext(6)
Collectlet (numberSignal, observer) = Signal<Int, NoError>.pipe()numberSignal.observeNext { (number) in print(number) // 1, 2, 3, 4, 5}numberSignal.collect { (values) -> Bool in return values.reduce(0, combine: +) > 4}.observeNext { (values) in print(values) // [1, 2, 3], [4 ,5]}observer.sendNext(1)observer.sendNext(2)observer.sendNext(3)observer.sendNext(4)observer.sendNext(5)
Manipulating multiple signals
Combine latest
Combine latest
let (numberSignal, numberObserver) = Signal<Int, NoError>.pipe()let (textSignal, textObserver) = Signal<String, NoError>.pipe()combineLatest(numberSignal, textSignal).observeNext { (number, text) in print("\(text) - \(number)")}numberObserver.sendNext(1) // Nothing printedtextObserver.sendNext("John") // John - 1numberObserver.sendNext(2) // John - 2textObserver.sendNext("Mary") // Mary - 2
Zip
Ziplet (menSignal, menObserver) = Signal<String, NoError>.pipe()let (womenSignal, womenObserver) = Signal<String, NoError>.pipe()let zippedSignal = zip(menSignal, womenSignal)zippedSignal.observeNext { (man, woman) in print("New couple - \(man) and \(woman)")}zippedSignal.observeCompleted({ print("Completed")})
menObserver.sendNext("John") // Nothing printedmenObserver.sendNext("Tom") // Nothing printedwomenObserver.sendNext("Lisa") // New couple - John and LisamenObserver.sendNext("Greg") // Nothing printedmenObserver.sendCompleted()womenObserver.sendNext("Sandra") // New couple - Tom and SandrawomenObserver.sendNext("Mary") // New couple - Greg and Mary, Completed
Merge
Mergelet (menSignal, menObserver) = Signal<String, NoError>.pipe()let (womenSignal, womenObserver) = Signal<String, NoError>.pipe()let (peopleSignal, peopleObserver) = Signal<Signal<String, NoError>, NoError>.pipe()peopleSignal.flatten(.Merge).observeNext { (name) in print(name)}peopleObserver.sendNext(menSignal)peopleObserver.sendNext(womenSignal)
menObserver.sendNext("John") // JohnwomenObserver.sendNext("Lisa") // Lisa
Handling errors
Catching errorslet (producer, observer) = SignalProducer<String, NSError>.buffer(5)let error = NSError(domain: "domain", code: 0, userInfo: nil)
producer .flatMapError { _ in SignalProducer<String, NoError>(value: "Default") } .startWithNext { next in print(next) }
observer.sendNext("First") // prints "First"observer.sendNext("Second") // prints "Second"observer.sendFailed(error) // prints "Default"
Retryvar tries = 0 let limit = 2 let error = NSError(domain: "domain", code: 0, userInfo: nil)let producer = SignalProducer<String, NSError> { (observer, _) in if tries++ < limit { observer.sendFailed(error) } else { observer.sendNext("Success") observer.sendCompleted() }}producer .on(failed: {e in print("Failure")}).retry(2).start { event in // prints "Failure" twice switch event { case let .Next(next): print(next) // prints "Success" case let .Failed(error): print("Failed: \(error)") } }
Promotinglet (numbersSignal, numbersObserver) = Signal<Int, NoError>.pipe()let (lettersSignal, lettersObserver) = Signal<String, NSError>.pipe()
numbersSignal .promoteErrors(NSError) .combineLatestWith(lettersSignal)
Mapping errors
Properties
Properties• AnyProperty
• ConstantProperty
• MutableProperty
• DynamicProperty
MutablePropertylet name = MutableProperty<String>("Bob")name.producer.startWithNext { (text) in print(text)}
name.modify { (name) -> String in return name + "!"}
name.value = "Lisa"
DynamicPropertylet textProperty = DynamicProperty(object: textField, keyPath: "text")
textProperty.producer.startWithNext { (text) in print(text)}
textProperty.value = "Textfield text"
Bindings
Basic bindinglet property = MutableProperty<String>("")let (producer, _) = SignalProducer<String, NoError>.buffer(1)let (signal, _) = Signal<String, NoError>.pipe()
property <~ producerproperty <~ signal
Action
Create Actionlet action = Action<Int, String, NSError>({ (number) -> SignalProducer<String, NSError> in return SignalProducer<String, NSError> {observer, disposable in observer.sendNext("Number is \(number)") observer.sendCompleted() }})
Create signal producerlet producer = action.apply(1)
Execute actionprodcuer.startWithSignal { (signal, disposable ) in signal.observeNext({ (value) in print("\(value)") }) signal.observeFailed({ (actionError) in print("\(actionError)") })}
Observing actionslet action = Action<Int, String, NSError>({ (number) -> SignalProducer<String, NSError> in return SignalProducer<String, NSError> {observer, disposable in observer.sendNext("Number is \(number)") observer.sendCompleted() }})action.values.observe { (value) in print("Value: \(value)")}action.errors.observe { (error) in print("Error: \(error)")}action.events.observe { (event) in print("Event: \(event)")}action.apply(5).startWithSignal { (_ , _ ) in }
CocoaAction
Prepare Actionvar text = MutableProperty<String>("Switch is on")let switchControl = UISwitch()let switchAction = Action<Bool, String, NoError>({ (isOn) -> SignalProducer<String, NoError> in return SignalProducer<String, NoError> { observer, disposable in observer.sendNext(isOn ? "Switch is on" : "Switch is off") observer.sendCompleted() }})
Create CocoaActionlet switchCocoaAction = CocoaAction(switchAction, { (control) -> Bool in let control = control as! UISwitch return control.on})
switchControl.addTarget(switchCocoaAction, action: CocoaAction.selector, forControlEvents: .ValueChanged)
text <~ switchAction.values
Schedulers• SchedulerType
• ImmediateScheduler• UIScheduler
• DateSchedulerType• QueueScheduler• TestScheduler
Memory Management
Disposables
Task
let producer = SignalProducer<String, NoError> { (observer, composite) in let date = NSDate().dateByAddingTimeInterval(10) composite += QueueScheduler().scheduleAfter(date, action: { print("Doing my work") // Doing my work observer.sendNext("Test") observer.sendCompleted() })}producer.startWithSignal { (signal, disposable) in signal.observeNext({ (value) in print(value) // Test }) signal.observeCompleted({ print("Work completed") // Work completed })}
Cancelling work
let producer = SignalProducer<String, NoError> { (observer, composite) in let date = NSDate().dateByAddingTimeInterval(10) composite += QueueScheduler().scheduleAfter(date, action: { print("Doing my work") // Not printed observer.sendNext("Test") observer.sendCompleted() })}
producer.startWithSignal { (signal, disposable) in signal.observeNext({ (value) in print(value) // Not printed })
signal.observeInterrupted({ print("Work interrupted") // Work interrupted }) let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { disposable.dispose() })}
Cleaninglet producer = SignalProducer<String, NoError> { (observer, composite) in composite.addDisposable({ print("I'm done") }) let date = NSDate().dateByAddingTimeInterval(4) composite += QueueScheduler().scheduleAfter(date, action: { print("Doing my work") // Not printed })}
producer.startWithSignal { (signal, disposable) in signal.observeInterrupted({ print("Work interrupted") })
let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { disposable.dispose() // Work interrupted, I'm done })}
Disposing signallet producer = SignalProducer<String, NoError> { (observer, composite) in composite.addDisposable({ print("I'm done") }) let date = NSDate().dateByAddingTimeInterval(5) composite += QueueScheduler().scheduleAfter(date, action: { print("Doing my work") // Not printed })}
producer.startWithSignal { (signal, disposable) in let signalDisposable = signal.observeInterrupted({ print("Work interrupted") // Not printed })
let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { signalDisposable!.dispose() }) let date2 = NSDate().dateByAddingTimeInterval(4) QueueScheduler().scheduleAfter(date2, action: { disposable.dispose() })}
Closures
What's the result?var value = 10let closure = { let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { print(value) })}closure()value = 20
Captured valuevar value = 10let closure = { [value] in let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { print(value) })}closure()value = 20
Weak, Strong, Unowned...
Unownedlet closure = { [unowned self] in self.label.text = "test" }
Weaklet closure = { [weak self] in guard let weakSelf = self else { return } self.label.text = "test" }
Rex
UIButtonlet cocoaAction = CocoaAction(action) { _ in }//without Rexbutton.addTarget(cocoaAction, action: CocoaAction.selector, forControlEvents: .TouchUpInside)//with Rex extensionsbutton.rex_pressed.value = cocoaAction
UITextField, UILabel, MutableProperty
var titleValue = MutableProperty<String?>(nil)//without RextextField.rac_textSignal().subscribeNext { self.titleValue.value = $0 as? String}titleValue.producer.startWithNext { self.label.text = $0 self.label.hidden = $0?.characters.count < 5}//with RextitleValue <~ textField.rex_texttitleLabel.rex_text <~ titleValuetitleLabel.rex_hidden <~ titleValue.producer.map( { $0?.characters.count < 5 })
Let's see it in action