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?
Wrapping in a Promise
What about using existing code, SDKs, or third-party libraries that don’t have PromiseKit support built in? Well, for that, PromiseKit comes with a promise wrapper.
Take, for instance, this app. Since there are a limited number of weather conditions, it’s not necessary to fetch the condition icon from the web every time; it’s inefficient and potentially costly.
In WeatherHelper.swift, there are already helper functions for saving and loading an image file from a local caches directory. These functions perform the file I/O on a background thread and use an asynchronous completion block when the operation finishes. This is a common pattern, so PromiseKit has a built-in way of handling it.
Replace getIcon(named:)
from WeatherHelper
with the following (again, ignore the compiler error about the missing method for now):
func getIcon(named iconName: String) -> Promise<UIImage> {
return Promise<UIImage> {
getFile(named: iconName, completion: $0.resolve) // 1
}
.recover { _ in // 2
self.getIconFromNetwork(named: iconName)
}
}
Here’s how this code works:
- You construct a Promise much like before, with one minor difference – you use the Promise’s
resolve
method instead offulfill
andreject
. SincegetFile(named:completion:)
‘s completion closure’s signature matches that of theresolve
method, passing down a reference to it will automatically take care of dealing with all resulting cases of the provided completion closure. - Here, if the icon doesn’t exist locally, the
recover
closure executes and you use another promise to fetch it over the network.
If a promise created with a value is not fulfilled, PromiseKit invokes its recover
closure. Otherwise, if the image is already loaded and ready to go, it’s available to return right away without calling recover
. This pattern is how you can create a promise that can either do something asynchronously (like load from the network) or synchronously (like use an in-memory value). This is useful when you have a locally cached value, such as an image.
To make this work, you’ll have to save the images to the cache when they come in. Add the following right below the previous method:
func getIconFromNetwork(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
return Promise {
self.saveFile(named: iconName, data: urlResponse.data, completion: $0.resolve)
}
.then(on: DispatchQueue.global(qos: .background)) {
return Promise.value(UIImage(data: urlResponse.data)!)
}
}
}
This is similar to the previous getIcon(named:)
except that in the dataPromise
‘s then
block, there is a call to saveFile
that you wrap just like you did in getFile
.
This uses a construct called firstly
. firstly
is syntactic sugar that simply executes its promise. It’s not really doing anything other than adding a layer of indirection for readability. Since the call to saveFile
is a just a side effect of loading the icon, using firstly
here enforces a little bit of ordering.
All in all, here’s what happens the first time you request an icon:
- First, make a URLSession request for the icon.
- Once that completes, save the data to a file.
- After the image is saved locally, turn the data into an image and send it down the chain.
If you build and run now, you shouldn’t see any difference in your app’s functionality, but you can check the file system to see that the images have been saved locally. To do that, search the console output for the term Saved image to:
. This will reveal the URL of the new file, which you can use to find its location on disk.
Ensuring Actions
Looking at the PromiseKit syntax, you might have asked: If there is a then
and a catch
, is there a way to share code and make sure an action always runs (like a cleanup task), regardless of success or failure? Well, there is: It’s called finally
.
In WeatherViewController.swift update handleLocation(city:state: coordinate:)
to show a network activity indicator in the status bar while you use your Promise to get the weather from the server.
Insert the following line before the call to weatherAPI.getWeather...
:
UIApplication.shared.isNetworkActivityIndicatorVisible = true
Then, chain the following to the end of your catch
closure:
.finally {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
}
This is the canonical example of when to use finally
. Regardless if the weather is completely loaded or if there is an error, the Promise responsible for network activity will end, so you should always dismiss the activity indicator when it does. Similarly, you can use this to close sockets, database connections or disconnect from hardware services.
Implementing Timers
One special case is a promise that’s fulfilled, not when some data is ready, but after a certain time interval. Currently, after the app loads the weather, it never refreshes. Change that to update the weather hourly.
In updateWithCurrentLocation()
, add the following code to the end of the method:
after(seconds: oneHour).done { [weak self] in
self?.updateWithCurrentLocation()
}
.after(seconds:)
creates a promise that completes after the specified number of seconds passes. Unfortunately, this is a one-shot timer. To do the update every hour, it was made recursive onupdateWithCurrentLocation()
.
Using Parallel Promises
So far, all promises discussed here have either been standalone or chained together in a sequence. PromiseKit also provides functionality for wrangling multiple promises fulfilling in parallel. There are two functions for waiting for multiple promises. The first – race
– returns a promise that is fulfilled when the first of a group of promises is fulfilled. In essence, the first one completed is the winner.
The other function is when
. It fulfills after all the specified promises are fulfilled. when(fulfilled:)
ends with a rejection as soon as any one of the promises do. There’s also a when(resolved:)
that waits for all promises to complete, but always calls the then
block and never the catch
.
race
, the race
‘s then
closure executes after the first promise completes. However, the other two unfulfilled promises keep executing until they, too, resolve.Take the contrived example of showing the weather in a “random” city. Since the user doesn’t care what city it will show, the app can try to fetch weather for multiple cities, but just handle the first one to complete. This gives the illusion of randomness.
Replace showRandomWeather(_:)
with the following:
@IBAction func showRandomWeather(_ sender: AnyObject) {
randomWeatherButton.isEnabled = false
let weatherPromises = randomCities.map {
weatherAPI.getWeather(atLatitude: $0.2, longitude: $0.3)
}
UIApplication.shared.isNetworkActivityIndicatorVisible = true
race(weatherPromises)
.then { [weak self] weatherInfo -> Promise<UIImage> in
guard let self = self else { return brokenPromise() }
self.placeLabel.text = weatherInfo.name
self.updateUI(with: weatherInfo)
return self.weatherAPI.getIcon(named: weatherInfo.weather.first!.icon)
}
.done { icon in
self.iconImageView.image = icon
}
.catch { error in
self.tempLabel.text = "--"
self.conditionLabel.text = error.localizedDescription
self.conditionLabel.textColor = errorColor
}
.finally {
UIApplication.shared.isNetworkActivityIndicatorVisible = false
self.randomWeatherButton.isEnabled = true
}
}
Here, you create a bunch of promises to fetch the weather for a selection of cities. These are then raced against each other with race(promises:)
. The then
closure executes only when the first of those promises fulfills. The done
block updates the image. If an error occurs, the catch
closure takes care of UI cleanup. Lastly, the remaining finally
ensures your activity indicator is cleared and button re-enabled.
In theory, this should be a random choice due to variation in server conditions, but it’s not a strong example. Also note that all of the promises will still resolve, so there are still five network calls, even though you only care about one.
Build and run. Once the app loads, tap Random Weather.