reactivecocoa workshop

97
Building iOS apps with ReactiveCocoa

Upload: eliasz-sawicki

Post on 09-Jan-2017

56 views

Category:

Technology


1 download

TRANSCRIPT

Page 1: ReactiveCocoa workshop

Building iOS apps with ReactiveCocoa

Page 2: ReactiveCocoa workshop

About me

Eliasz SawickiBlog: http://eluss.github.io/Twitter: @EliSawic

Page 3: ReactiveCocoa workshop

Let's begin

Page 4: ReactiveCocoa workshop

ReactiveCocoa

Page 5: ReactiveCocoa workshop

Functional Reactive Programming

Page 6: ReactiveCocoa workshop

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

Page 7: ReactiveCocoa workshop

Reactive Programming

Page 8: ReactiveCocoa workshop

Reactive Programming• Working with asynchronous dataflow

• Reacting to state changes

Page 9: ReactiveCocoa workshop

Functional Programming

Page 10: ReactiveCocoa workshop

Functional Programming• Immutable

Page 11: ReactiveCocoa workshop

assert(f(x) == f(x))

Page 12: ReactiveCocoa workshop

A personclass Person { var name: String init(name: String) { self.name = name }}

Page 13: ReactiveCocoa workshop

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

Page 14: ReactiveCocoa workshop

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

Page 15: ReactiveCocoa workshop

Functional Programming• Immutable• Stateless

Page 16: ReactiveCocoa workshop

Statefulvar value = 0

func increment() { value += 1}

Page 17: ReactiveCocoa workshop

Statelessfunc increment(value: Int) -> Int { return value + 1}

Page 18: ReactiveCocoa workshop

Declarative

Page 19: ReactiveCocoa workshop

Imperative vs

Declarative

Page 20: ReactiveCocoa workshop

Imperativelet array = [0, 1, 2, 3, 4, 5]var evenNumbers = [Int]()for element in array { if element % 2 == 0 { evenNumbers.append(element) }}

Page 21: ReactiveCocoa workshop

Declarativelet array = [0, 1, 2, 3, 4, 5]let evenNumbers = array.filter { $0 % 2 == 0 }

Page 22: ReactiveCocoa workshop

Back to ReactiveCocoa

Page 23: ReactiveCocoa workshop

Event streams

Page 24: ReactiveCocoa workshop

Event Stream

Page 25: ReactiveCocoa workshop

Event

Page 26: ReactiveCocoa workshop

Non-Terminating

• Next

Terminating

• Completed

• Failed

• Interrupted

Page 27: ReactiveCocoa workshop

Observer

Page 28: ReactiveCocoa workshop

Signal

Page 29: ReactiveCocoa workshop

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

Page 30: ReactiveCocoa workshop

Signal's lifetime• Passes any number of Next events

• "Dies" when terminating event arrives

• Any new observer will receive Interrupted event

Page 31: ReactiveCocoa workshop

Observingsignal.observe { (event) in print(event)}signal.observeNext { (value) in print(value)}

signal.observeCompleter { print("Completed")}

Page 32: ReactiveCocoa workshop

Creating Signals

Page 33: ReactiveCocoa workshop

Basic signalSignal<String, NSError> { (observer) -> Disposable? in observer.sendNext("test") observer.sendCompleted() return ActionDisposable(action: { print("Signal disposed") })}

Page 34: ReactiveCocoa workshop

Pipe

Page 35: ReactiveCocoa workshop

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

Page 36: ReactiveCocoa workshop

SignalProducer

Page 37: ReactiveCocoa workshop

What is it?• Represents tasks

• Creates signals

• Performs side effects

• Does not start it's work if not started

Page 38: ReactiveCocoa workshop

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

Page 39: ReactiveCocoa workshop

Creating Signal Producers

Page 40: ReactiveCocoa workshop

Basic Signal ProducerSignalProducer<String, NSError> { (observer, composite) in composite.addDisposable({ print("Clearing work") })

observer.sendNext("In Progres...") observer.sendCompleted()}

Page 41: ReactiveCocoa workshop

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

Page 42: ReactiveCocoa workshop

Manipulating signals

Page 43: ReactiveCocoa workshop

Map

Page 44: ReactiveCocoa workshop

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)

Page 45: ReactiveCocoa workshop

Filter

Page 46: ReactiveCocoa workshop

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)

Page 47: ReactiveCocoa workshop

Aggregating

Page 48: ReactiveCocoa workshop

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

Page 49: ReactiveCocoa workshop

Skip repeats

Page 50: ReactiveCocoa workshop

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)

Page 51: ReactiveCocoa workshop

Skip until

Page 52: ReactiveCocoa workshop

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)

Page 53: ReactiveCocoa workshop

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)

Page 54: ReactiveCocoa workshop

Manipulating multiple signals

Page 55: ReactiveCocoa workshop

Combine latest

Page 56: ReactiveCocoa workshop

Combine latest

Page 57: ReactiveCocoa workshop

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

Page 58: ReactiveCocoa workshop

Zip

Page 59: ReactiveCocoa workshop

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

Page 60: ReactiveCocoa workshop

Merge

Page 61: ReactiveCocoa workshop

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

Page 62: ReactiveCocoa workshop

Handling errors

Page 63: ReactiveCocoa workshop

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"

Page 64: ReactiveCocoa workshop

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

Page 65: ReactiveCocoa workshop

Promotinglet (numbersSignal, numbersObserver) = Signal<Int, NoError>.pipe()let (lettersSignal, lettersObserver) = Signal<String, NSError>.pipe()

numbersSignal .promoteErrors(NSError) .combineLatestWith(lettersSignal)

Page 66: ReactiveCocoa workshop

Mapping errors

Page 67: ReactiveCocoa workshop

Properties

Page 68: ReactiveCocoa workshop

Properties• AnyProperty

• ConstantProperty

• MutableProperty

• DynamicProperty

Page 69: ReactiveCocoa workshop

MutablePropertylet name = MutableProperty<String>("Bob")name.producer.startWithNext { (text) in print(text)}

name.modify { (name) -> String in return name + "!"}

name.value = "Lisa"

Page 70: ReactiveCocoa workshop

DynamicPropertylet textProperty = DynamicProperty(object: textField, keyPath: "text")

textProperty.producer.startWithNext { (text) in print(text)}

textProperty.value = "Textfield text"

Page 71: ReactiveCocoa workshop

Bindings

Page 72: ReactiveCocoa workshop

Basic bindinglet property = MutableProperty<String>("")let (producer, _) = SignalProducer<String, NoError>.buffer(1)let (signal, _) = Signal<String, NoError>.pipe()

property <~ producerproperty <~ signal

Page 73: ReactiveCocoa workshop

Action

Page 74: ReactiveCocoa workshop

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

Page 75: ReactiveCocoa workshop

Create signal producerlet producer = action.apply(1)

Page 76: ReactiveCocoa workshop

Execute actionprodcuer.startWithSignal { (signal, disposable ) in signal.observeNext({ (value) in print("\(value)") }) signal.observeFailed({ (actionError) in print("\(actionError)") })}

Page 77: ReactiveCocoa workshop

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 }

Page 78: ReactiveCocoa workshop

CocoaAction

Page 79: ReactiveCocoa workshop

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

Page 80: ReactiveCocoa workshop

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

Page 81: ReactiveCocoa workshop

Schedulers• SchedulerType

• ImmediateScheduler• UIScheduler

• DateSchedulerType• QueueScheduler• TestScheduler

Page 82: ReactiveCocoa workshop

Memory Management

Page 83: ReactiveCocoa workshop

Disposables

Page 84: ReactiveCocoa workshop

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

Page 85: ReactiveCocoa workshop

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

Page 86: ReactiveCocoa workshop

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

Page 87: ReactiveCocoa workshop

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

Page 88: ReactiveCocoa workshop

Closures

Page 89: ReactiveCocoa workshop

What's the result?var value = 10let closure = { let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { print(value) })}closure()value = 20

Page 90: ReactiveCocoa workshop

Captured valuevar value = 10let closure = { [value] in let date = NSDate().dateByAddingTimeInterval(2) QueueScheduler().scheduleAfter(date, action: { print(value) })}closure()value = 20

Page 91: ReactiveCocoa workshop

Weak, Strong, Unowned...

Page 92: ReactiveCocoa workshop

Unownedlet closure = { [unowned self] in self.label.text = "test" }

Page 93: ReactiveCocoa workshop

Weaklet closure = { [weak self] in guard let weakSelf = self else { return } self.label.text = "test" }

Page 94: ReactiveCocoa workshop

Rex

Page 95: ReactiveCocoa workshop

UIButtonlet cocoaAction = CocoaAction(action) { _ in }//without Rexbutton.addTarget(cocoaAction, action: CocoaAction.selector, forControlEvents: .TouchUpInside)//with Rex extensionsbutton.rex_pressed.value = cocoaAction

Page 96: ReactiveCocoa workshop

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

Page 97: ReactiveCocoa workshop

Let's see it in action