Premise: Promises

A presentation at Motor City Cocoaheads in April 2018 in Detroit, MI, USA by Anne Cahalan

Slide 1

Slide 1

PREMISE: PROMISES

Slide 2

Slide 2

! HI, I'M ANNE.

Slide 3

Slide 3

WHY PROMISES?

Slide 4

Slide 4

BRACE YOURSELF, I'M GOING TO SAY SOMETHING NICE ABOUT JAVASCRIPT

Slide 5

Slide 5

FIRST STOP IS ALWAYS WIKIPEDIA

First used in 1976-77

Slide 6

Slide 6

BARBARA LISKOV IS KINDA RAD

First used in 1976-77

Pipelining/Chaining invented in 1988

Slide 7

Slide 7

Y2K REVIVAL

First used in 1976-77

Pipelining/Chaining invented in 1988

Resurgence of interest in 2000

Slide 8

Slide 8

WHAT IS A PROMISE AND WHY WOULD I WANT ONE?

represents the the eventual result of an asynchronous task

or the error if that task fails

a way of formalizing completion handlers to chain asynchronous tasks

Slide 9

Slide 9

SO I MADE AN APP

Slide 10

Slide 10

I HIT UP THE REST COUNTRIES API TO GET A LIST OF ALL THE COUNTRIES IN THE WORLD...

Slide 11

Slide 11

THEN I FOUND A WEATHER API AND A CURRENCY EXCHANGE API TO PLAY WITH

Slide 12

Slide 12

COUNTRYLIST VIEW CONTROLLER

Slide 13

Slide 13

func fetchAllCountries (handler: @escaping ([Country]?) -> ()) {

guard

let url = URL (string: allCountriesURLString) else { return }

let urlRequest = URLRequest (url: url)

let session = URLSession .shared

let task = session.dataTask(with: urlRequest, completionHandler: { data, response, error in

guard error == nil

else {

print ( " ! request error" )

return }

guard

let responseData = data else {

print ( " ! data response error" )

return }

let countryArray: [ Country ] = self .decodeAllCountries(countryData: responseData) handler(countryArray) }) task.resume() }

Slide 14

Slide 14

override

func viewDidLoad () {

super .viewDidLoad()

let networker = Networker () networker.fetchAllCountries { countries in

guard

let list = countries else { return }

self .countryList = list

DispatchQueue .main.async {

self .tableView.reloadData() } } }

Slide 15

Slide 15

SELECTED COUNTRY VIEW CONTROLLER

Slide 16

Slide 16

func fetchCurrentExchangeRate (currencyCode: String, handler: @escaping (ExchangeRate?) -> ()) {

guard

let currencyURL = URL (string: currencyConversionBaseURLString + currencyAccessKey + "&currencies=(currencyCode)&format=1" ) else {

print ( " ! currency url error" )

return }

let urlRequest = URLRequest (url: currencyURL)

let session = URLSession .shared

let task = session.dataTask(with: urlRequest, completionHandler: { data, response, error in

guard error == nil

else {

print ( " ! request error: (String(describing: error))" )

return }

guard

let responseData = data else {

print ( " ! data response error" )

return }

guard

let exchangeRate: ExchangeRate

self .decodeExchangeRateData(currencyData: responseData) else {

print ( " ! decoding error" )

return } handler(exchangeRate) }) task.resume() }

Slide 17

Slide 17

↩ SIDE TRIP! THIS IS WHY YOU GO TO MEETUPS!

Slide 18

Slide 18

FUN WITH CODABLE Botswana Pula Danish Krone Polish Z ł oty "USDBWP":9.591896 "USDDKK":6.04103 "USDPLN":3.388799

Slide 19

Slide 19

FUN WITH CODABLE struct Quote: Codable {

var conversion: String

""

var rate: Float

0.0 }

Slide 20

Slide 20

FUN WITH CODABLE extension Quote {

struct QuoteKeys: CodingKey {

var stringValue: String

var intValue: Int ?

init ?(stringValue: String ) {

self .stringValue = stringValue }

init ?(intValue: Int ) {

return

nil } }

public

init (from decoder: Decoder ) throws {

let container = try decoder.container(keyedBy: QuoteKeys . self )

for key in container.allKeys {

self .conversion = key.stringValue

self .rate = try container.decode( Float . self , forKey: key) } } }

Slide 21

Slide 21

SELECTED COUNTRY VIEW CONTROLLER

Slide 22

Slide 22

private

func vanillaNetworkingGetTheStuff () {

// guard some optional business networker.fetchCurrentExchangeRate(currencyCode: currencyCode) { rate in

self .exchangeRate = rate

DispatchQueue .main.async {

self .setupExchangeRateUI() } } networker.fetchCapitalCityWeather(country: country) { weather in

self .weather = weather

DispatchQueue .main.async {

self .setupWeatherUI() }

guard

let iconCode = self .weather?.conditions.first?.iconCode else {

print ( " ! error unwrapping icon code" )

return }

self .networker.fetchWeatherIcon(iconCode: iconCode) { weatherImage in

DispatchQueue .main.async {

self .weatherIconImageView.image = weatherImage } } } }

Slide 23

Slide 23

! SOME CONCERNS

Two separate network calls

That could end at two different times

A third network call that depends on one of the others

Slide 24

Slide 24

LET'S TRY PROMISES

Slide 25

Slide 25

RETURN A PROMISE OF A TYPE func fetchAllCountries (handler: @escaping ([Country]?) -> ()) vs func promiseFetchAllCountries () -> Promise <[ Country ]>

Slide 26

Slide 26

FULFILL OR REJECT func promiseFetchAllCountries () -> Promise <[ Country ]> {

// some url and session business

return

Promise { seal in

let task = session.dataTask(with: urlRequest) { data, _ , error in

if

let responseData = data {

let allCountries = self .decodeAllCountries(countryData: responseData) seal.fulfill(allCountries) } else

if

let requestError = error { seal.reject(requestError) } } task.resume() } }

Slide 27

Slide 27

↪ SIDE TRIP! THE MARCH OF PROGRESS VS. THE INTERNET IS FOREVER

Slide 28

Slide 28

PROMISEKIT 6.0 INCLUDED A MAJOR CHANGE IN THE PROMISE INITIALIZER

Slide 29

Slide 29

FROM Promise { fulfill, reject in

//… } TO Promise { seal in

// ... }

Slide 30

Slide 30

COUNTRYLIST VIEW CONTROLLER

Slide 31

Slide 31

override

func viewDidLoad () {

super .viewDidLoad()

let networker = Networker () firstly { networker.promiseFetchAllCountries() }.done { countryArray in

self .countryList = countryArray

self .tableView.reloadData() }. catch { error in

print ( " ! some kind of error listing all countries -> (error)" ) } }

Slide 32

Slide 32

! ENTER THE CIRCLE OF SHARING !

Slide 33

Slide 33

handler IS GARBAGE

Slide 34

Slide 34

I MEAN, LOOK AT THIS: func fetchAllCountries (handler: @escaping ([Country]?) -> ())

Slide 35

Slide 35

LOOK AT THIS MESS: func fetchAllCountries (handler: @escaping ([Country]?) -> ())

is this clean code?

Slide 36

Slide 36

AND WHAT ABOUT THIS: func duckBusiness () { doAThing { quackLikeADuck() } } func doAThing (handler: () -> ()) { doSomeStuff() doAnotherThing { handler() } } func doAnotherThing (handler: () ->()) {
doSomeMoreThings() handler() }

Slide 37

Slide 37

UGH. IT'S THE WORST: func fetchAllCountries (handler: @escaping ([Country]?) -> ())

is this clean code?

chaining is impossible

fuckingblocksyntax.com and

fuckingclosuresyntax.com

Slide 38

Slide 38

! THANK YOU FOR SHARING !

Slide 39

Slide 39

! PROMISES HANDLE handler

Slide 40

Slide 40

CHECK THIS OUT firstly { networker.promiseFetchAllCountries() }.done { countryArray in

self .countryList = countryArray

self .tableView.reloadData() }. catch { error in

print ( " ! some kind of error listing all countries -> (error)" ) }

Slide 41

Slide 41

THAT WAS EASY MODE. WHAT ABOUT THE HARD STUFF?

Slide 42

Slide 42

func promiseFetchCurrentExchangeRate (currencyCode: String) -> Promise < ExchangeRate

{ ... } func promiseFetchCapitalCityWeather (country: Country) -> Promise < Weather { ... } func promiseFetchWeatherIcon (iconCode: String) -> Promise < UIImage { ... }

Slide 43

Slide 43

firstly { when(fulfilled: networker.promiseFetchCurrentExchangeRate(currencyCode: currencyCode), networker.promiseFetchCapitalCityWeather(country: country)) }.done { exchangeRate, weather in

self .exchangeRate = exchangeRate

self .weather = weather

guard

let iconCode = weather.conditions.first?.iconCode else { return }

self .networker.promiseFetchWeatherIcon(iconCode: iconCode).done { weatherImage in

self .weatherIconImageView.image = weatherImage } }. catch { error in

print ( " ! error in getting the data for (String(describing: country.name)) -> (error)" ) }.finally {

self .setupExchangeRateUI()

self .setupWeatherUI()

self .activityIndicator.stopAnimating() }

Slide 44

Slide 44

WHEN when(fulfilled: networker.promiseFetchCurrentExchangeRate(currencyCode: currencyCode), networker.promiseFetchCapitalCityWeather(country: country) )

Slide 45

Slide 45

DONE .done { exchangeRate, weather in

self .exchangeRate = exchangeRate

self .weather = weather

guard

let iconCode = weather.conditions.first?.iconCode else { return }

self .networker.promiseFetchWeatherIcon(iconCode: iconCode).done { weatherImage in

self .weatherIconImageView.image = weatherImage }

Slide 46

Slide 46

CATCH . catch { error in

print ( " ! some kind of error in getting the data for (String(describing: country.name)) -> (error)" ) }

Slide 47

Slide 47

FINALLY .finally {

self .setupExchangeRateUI()

self .setupWeatherUI()

self .activityIndicator.stopAnimating() }

Slide 48

Slide 48

firstly { when(fulfilled: networker.promiseFetchCurrentExchangeRate(currencyCode: currencyCode), networker.promiseFetchCapitalCityWeather(country: country)) }.done { exchangeRate, weather in

self .exchangeRate = exchangeRate

self .weather = weather

guard

let iconCode = weather.conditions.first?.iconCode else { return }

self .networker.promiseFetchWeatherIcon(iconCode: iconCode).done { weatherImage in

self .weatherIconImageView.image = weatherImage } }. catch { error in

print ( " ! error in getting the data for (String(describing: country.name)) -> (error)" ) }.finally {

self .setupExchangeRateUI()

self .setupWeatherUI()

self .activityIndicator.stopAnimating() }

Slide 49

Slide 49

WAIT A SECOND... self .networker.promiseFetchWeatherIcon(iconCode: iconCode) .done { weatherImage in

self .weatherIconImageView.image = weatherImage }

Slide 50

Slide 50

firstly {
when(fulfilled: networker.promiseFetchCurrentExchangeRate(currencyCode: currencyCode), networker.promiseFetchCapitalCityWeather(country: country)) }.then { exchangeRate, weather in

self .exchangeRate = exchangeRate

self .weather = weather

guard

let iconCode = weather.conditions.first?.iconcode else { return } networker.promiseFetchWeatherIcon(iconCode: iconCode) }.done { weatherImage in

self .weatherIconImageView.image = weatherImage }. catch { error in

print ( " ! error in getting the data for (String(describing: country.name)) -> (error)" ) }.finally {

self .setupExchangeRateUI()

self .setupWeatherUI()

self .activityIndicator.stopAnimating() }

Slide 51

Slide 51

❗ Ambigious reference to member 'firstly(execute:)'

Slide 52

Slide 52

!

CHANGE YOUR TOOLS, CHANGE YOUR MIND

Slide 53

Slide 53

PROS & CONS

Slide 54

Slide 54

! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT

Slide 55

Slide 55

! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT ! SYNTAX IS CLEARLY MORE READABLE

Slide 56

Slide 56

! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT ! SYNTAX IS CLEARLY MORE READABLE ! NEW(ISH) IDEA AROUND AN OLD PROBLEM

Slide 57

Slide 57

! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT ! SYNTAX IS CLEARLY MORE READABLE ! NEW(ISH) IDEA AROUND AN OLD PROBLEM ! PROMISEKIT IS...OKAY

Slide 58

Slide 58

THE OTHER OPTIONS

Google Promises

Slide 59

Slide 59

THE OTHER OPTIONS

Google Promises

BrightFutures

Slide 60

Slide 60

THE OTHER OPTIONS

Google Promises

BrightFutures

Hydra

Slide 61

Slide 61

IF I HAD TO DO IT ALL OVER AGAIN...

Slide 62

Slide 62

QUESTIONS ❓

Slide 63

Slide 63

THANKS! ! @northofnormal ✉ northofnormal

github.com/northofnormal/PromisesPromises