PREMISE: PROMISES
A presentation at Motor City Cocoaheads in April 2018 in Detroit, MI, USA by Anne Cahalan
PREMISE: PROMISES
! HI, I'M ANNE.
WHY PROMISES?
BRACE YOURSELF, I'M GOING TO SAY SOMETHING NICE ABOUT JAVASCRIPT
FIRST STOP IS ALWAYS WIKIPEDIA
First used in 1976-77
BARBARA LISKOV IS KINDA RAD
First used in 1976-77
Pipelining/Chaining invented in 1988
Y2K REVIVAL
First used in 1976-77
Pipelining/Chaining invented in 1988
Resurgence of interest in 2000
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
SO I MADE AN APP
I HIT UP THE REST COUNTRIES API TO GET A LIST OF ALL THE COUNTRIES IN THE WORLD...
THEN I FOUND A WEATHER API AND A CURRENCY EXCHANGE API TO PLAY WITH
COUNTRYLIST VIEW CONTROLLER
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() }
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() } } }
SELECTED COUNTRY VIEW CONTROLLER
func fetchCurrentExchangeRate (currencyCode: String, handler: @escaping (ExchangeRate?) -> ()) {
guard
let currencyURL = URL (string: currencyConversionBaseURLString + currencyAccessKey + "¤cies=(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
self .decodeExchangeRateData(currencyData: responseData) else {
print ( " ! decoding error" )
return } handler(exchangeRate) }) task.resume() }
↩ SIDE TRIP! THIS IS WHY YOU GO TO MEETUPS!
FUN WITH CODABLE Botswana Pula Danish Krone Polish Z ł oty "USDBWP":9.591896 "USDDKK":6.04103 "USDPLN":3.388799
FUN WITH CODABLE struct Quote: Codable {
""
0.0 }
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) } } }
SELECTED COUNTRY VIEW CONTROLLER
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 } } } }
! SOME CONCERNS
Two separate network calls
That could end at two different times
A third network call that depends on one of the others
LET'S TRY PROMISES
RETURN A PROMISE OF A TYPE func fetchAllCountries (handler: @escaping ([Country]?) -> ()) vs func promiseFetchAllCountries () -> Promise <[ Country ]>
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() } }
↪ SIDE TRIP! THE MARCH OF PROGRESS VS. THE INTERNET IS FOREVER
PROMISEKIT 6.0 INCLUDED A MAJOR CHANGE IN THE PROMISE INITIALIZER
FROM Promise { fulfill, reject in
//… } TO Promise { seal in
// ... }
COUNTRYLIST VIEW CONTROLLER
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)" ) } }
! ENTER THE CIRCLE OF SHARING !
handler IS GARBAGE
I MEAN, LOOK AT THIS: func fetchAllCountries (handler: @escaping ([Country]?) -> ())
LOOK AT THIS MESS: func fetchAllCountries (handler: @escaping ([Country]?) -> ())
is this clean code?
AND WHAT ABOUT THIS:
func
duckBusiness
()
{
doAThing {
quackLikeADuck()
}
}
func
doAThing
(handler: ()
-> ()) {
doSomeStuff()
doAnotherThing {
handler()
}
}
func
doAnotherThing
(handler: ()
->()) {
doSomeMoreThings()
handler()
}
UGH. IT'S THE WORST: func fetchAllCountries (handler: @escaping ([Country]?) -> ())
is this clean code?
chaining is impossible
fuckingblocksyntax.com and
fuckingclosuresyntax.com
! THANK YOU FOR SHARING !
! PROMISES HANDLE handler
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)" ) }
THAT WAS EASY MODE. WHAT ABOUT THE HARD STUFF?
func promiseFetchCurrentExchangeRate (currencyCode: String) -> Promise < ExchangeRate
{ ... } func promiseFetchCapitalCityWeather (country: Country) -> Promise < Weather { ... } func promiseFetchWeatherIcon (iconCode: String) -> Promise < UIImage { ... }
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() }
WHEN when(fulfilled: networker.promiseFetchCurrentExchangeRate(currencyCode: currencyCode), networker.promiseFetchCapitalCityWeather(country: country) )
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 }
CATCH . catch { error in
print ( " ! some kind of error in getting the data for (String(describing: country.name)) -> (error)" ) }
FINALLY .finally {
self .setupExchangeRateUI()
self .setupWeatherUI()
self .activityIndicator.stopAnimating() }
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() }
WAIT A SECOND... self .networker.promiseFetchWeatherIcon(iconCode: iconCode) .done { weatherImage in
self .weatherIconImageView.image = weatherImage }
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() }
❗ Ambigious reference to member 'firstly(execute:)'
!
➡
CHANGE YOUR TOOLS, CHANGE YOUR MIND
PROS & CONS
! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT
! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT ! SYNTAX IS CLEARLY MORE READABLE
! SEEMS LIKE A LOT OF OVERHEAD FOR A SMALL PROJECT ! SYNTAX IS CLEARLY MORE READABLE ! NEW(ISH) IDEA AROUND AN OLD PROBLEM
! 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
THE OTHER OPTIONS
Google Promises
THE OTHER OPTIONS
Google Promises
BrightFutures
THE OTHER OPTIONS
Google Promises
BrightFutures
Hydra
IF I HAD TO DO IT ALL OVER AGAIN...
QUESTIONS ❓
THANKS! ! @northofnormal ✉ northofnormal
github.com/northofnormal/PromisesPromises