Getting Started With PromiseKit
Asynchronous programming can be a real pain and can easily result in messy code. Fortunately for you, there’s a better way using promises & PromiseKit on iOS. By Owen L Brown.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Getting Started With PromiseKit
30 mins
- Getting Started
- The OpenWeatherMap API
- Trying It Out
- Understanding Promises
- What PromiseKit… Promises
- Making Promises
- Using PromiseKit Wrappers
- Adding Location
- Searching for an Arbitrary Location
- Threading
- Wrapping in a Promise
- Ensuring Actions
- Implementing Timers
- Using Parallel Promises
- Where to Go From Here?
Using PromiseKit Wrappers
This is pretty good, but PromiseKit can do better. In addition to the code for Promise
, PromiseKit also includes extensions for common iOS SDK methods that can be expressed as promises. For example, the URLSession
data task method returns a promise instead of using a completion block.
In WeatherHelper.swift, replace the new getWeather(atLatitude:longitude:)
with the following code:
func getWeather(
atLatitude latitude: Double,
longitude: Double
) -> Promise<WeatherInfo> {
let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=" +
"\(latitude)&lon=\(longitude)&appid=\(appID)"
let url = URL(string: urlString)!
return firstly {
URLSession.shared.dataTask(.promise, with: url)
}.compactMap {
return try JSONDecoder().decode(WeatherInfo.self, from: $0.data)
}
}
See how easy it is to use PromiseKit wrappers? Much cleaner! Breaking it down:
PromiseKit provides a new overload of URLSession.dataTask(_:with:)
that returns a specialized Promise
representing a URL request. Note that the data promise automatically starts its underlying data task.
Next, PromiseKit’s compactMap
is chained to decode the data as a WeatherInfo
object and return it from the closure. compactMap
takes care of wrapping this result in a Promise
for you, so you can keep chaining additional promise-related methods.
Adding Location
Now that the networking is bullet-proofed, take a look at the location functionality. Unless you’re lucky enough to be visiting Athens, the app isn’t giving you particularly relevant data. Change your code to use the device’s current location.
In WeatherViewController.swift, replace updateWithCurrentLocation()
with the following:
private func updateWithCurrentLocation() {
locationHelper.getLocation()
.done { [weak self] placemark in // 1
self?.handleLocation(placemark: placemark)
}
.catch { [weak self] error in // 2
guard let self = self else { return }
self.tempLabel.text = "--"
self.placeLabel.text = "--"
switch error {
case is CLError where (error as? CLError)?.code == .denied:
self.conditionLabel.text = "Enable Location Permissions in Settings"
self.conditionLabel.textColor = UIColor.white
default:
self.conditionLabel.text = error.localizedDescription
self.conditionLabel.textColor = errorColor
}
}
}
Going over the above code:
- You use a helper class to work with Core Location. You’ll implement it in a moment. The result of
getLocation()
is a promise to get a placemark for the current location. - This catch block demonstrates how you handle different errors within a single catch block. Here, you use a simple
switch
to provide a different message when the user hasn’t granted location privileges versus other types of errors.
Next, in LocationHelper.swift replace getLocation()
with this:
func getLocation() -> Promise<CLPlacemark> {
// 1
return CLLocationManager.requestLocation().lastValue.then { location in
// 2
return self.coder.reverseGeocode(location: location).firstValue
}
}
This takes advantage of two PromiseKit concepts already discussed: SDK wrapping and chaining.
In the above code:
-
CLLocationManager.requestLocation()
returns a promise of the current location. - Once the current location is available, your chain sends it to
CLGeocoder.reverseGeocode(location:)
, which also returns a Promise to provide the reverse-coded location.
With promises, you link two different asynchronous actions in three lines of code! You require no explicit error handling here because the caller’s catch
block handles all of the errors.
Build and run. After accepting the location permissions, the app shows the current temperature for your (simulated) location. Voilà!
Searching for an Arbitrary Location
That’s all well and good, but what if a user wants to know the temperature somewhere else?
In WeatherViewController.swift, replace textFieldShouldReturn(_:)
with the following (ignore the compiler error about the missing method, for now):
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
guard let text = textField.text else { return false }
locationHelper.searchForPlacemark(text: text)
.done { placemark in
self.handleLocation(placemark: placemark)
}
.catch { _ in }
return true
}
This uses the same pattern as all the other promises: Find the placemark and, when that’s done, update the UI.
Next, add the following to LocationHelper.swift, below getLocation()
:
func searchForPlacemark(text: String) -> Promise<CLPlacemark> {
return coder.geocode(text).firstValue
}
It’s that simple! PromiseKit already has an extension for CLGeocoder
to find a placemark that returns a promise with a placemark.
Build and run. This time, enter a city name in the search field at the top and press Return. This should then find the weather for the best match for that name.
Threading
So far, you’ve taken one thing for granted: All then
blocks execute on the main thread. This is a great feature since most of the work in the view controller updates the UI. However, it’s best to handle long-running tasks on a background thread, so as not to make the app slow to respond to a user’s action.
You’ll next add a weather icon from OpenWeatherMap to illustrate the current weather conditions. However, decoding raw Data
into a UIImage
is a heavy task, which you wouldn’t want to perform on your main thread.
Back in WeatherHelper.swift, add the following method right after getWeather(atLatitude:longitude:)
:
func getIcon(named iconName: String) -> Promise<UIImage> {
let urlString = "http://openweathermap.org/img/w/\(iconName).png"
let url = URL(string: urlString)!
return firstly {
URLSession.shared.dataTask(.promise, with: url)
}
.then(on: DispatchQueue.global(qos: .background)) { urlResponse in
Promise.value(UIImage(data: urlResponse.data)!)
}
}
Here, you build a UIImage
from the loaded Data
on a background queue by supplying a DispatchQueue
via the on
parameter to then(on:execute:)
. PromiseKit then performs the then
block on provided queue.
Now, your promise runs on the background queue, so the caller will need to make sure the UI updates on the main queue.
Back in WeatherViewController.swift, replace the call to getWeather(atLatitude:longitude:)
inside handleLocation(city:state: coordinate:)
with this:
// 1
weatherAPI.getWeather(
atLatitude: coordinate.latitude,
longitude: coordinate.longitude)
.then { [weak self] weatherInfo -> Promise<UIImage> in
guard let self = self else { return brokenPromise() }
self.updateUI(with: weatherInfo)
// 2
return self.weatherAPI.getIcon(named: weatherInfo.weather.first!.icon)
}
// 3
.done(on: DispatchQueue.main) { icon in
self.iconImageView.image = icon
}
.catch { error in
self.tempLabel.text = "--"
self.conditionLabel.text = error.localizedDescription
self.conditionLabel.textColor = errorColor
}
There are three subtle changes to this call:
- First, you change the
getWeather(atLatitude:longitude:)
‘sthen
block to return aPromise
instead ofVoid
. This means that, when thegetWeather
promise completes, you return a new promise. - You use your just-added
getIcon
method to create a new promise to get the icon. - You add a new
done
closure to the chain, which will execute on the main queue when thegetIcon
promise completes.
DispatchQueue.main
for your done
block. By default, everything runs on the main queue. It’s included here to reinforce that fact.Thereby, you can chain promises into a sequence of serially executing steps. After one promise is fulfilled, the next will execute and so on until the final done
or an error occurs and the catch
executes instead. The two big advantages of this approach over nested completions are:
- You compose the promises in a single chain, which is easy to read and maintain. Each
then/done
block has its own context, keeping logic and state from bleeding into each other. A column of blocks is easier to read without an ever-deepening indent. - You handle all the errors in one spot. For example, in a complicated workflow like a user login, a single retry error dialog can display if any step fails.
Build and run. Image icons should now load!