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?
Asynchronous programming can be a real pain in the lemon. Unless you’re extremely careful, it can easily result in humongous delegates, messy completion handlers and long nights debugging code! But there’s a better way: promises. Promises tame asynchronicity by letting you write code as a series of actions based on events. This works especially well for actions that must occur in a certain order. In this PromiseKit tutorial, you’ll learn how to use the third-party PromiseKit to clean up your asynchronous code — and your sanity.
Typically, iOS programming involves many delegates and callbacks. You’ve likely seen a lot of code along these lines:
- Y manages X.
- Tell Y to get X.
- Y notifies its delegate when X is available.
Promises attempt to simplify this mess to look more like this:
When X is available, do Y.
Doesn’t that look delightful? Promises also let you separate error and success handling, which makes it easier to write clean code that handles many different conditions. They work great for complicated, multi-step workflows like logging into web services, performing authenticated SDK calls, processing and displaying images, and more!
Promises are becoming more common, with many available solutions and implementations. In this tutorial, you’ll learn about promises through using a popular, third-party Swift library called PromiseKit.
Getting Started
The project for this tutorial, WeatherOrNot, is a simple current weather app. It uses OpenWeatherMap for its weather API. You can translate the patterns and concepts for accessing this API to any other web service.
Start by downloading the project materials by using the Download Materials button at the top or bottom of this tutorial.
Your starter project already has PromiseKit bundled using CocoaPods, so there’s no need to install it yourself. If you haven’t used CocoaPods before and would like to learn about it, you can read our tutorial on it. However, this tutorial doesn’t require any knowledge about CocoaPods.
Open WeatherOrNot.xcworkspace, and you’ll see that the project is very simple. It only has five .swift files:
- AppDelegate.swift: An auto-generated app delegate file.
- BrokenPromise.swift: A placeholder promise used to stub some parts of the starter project.
- WeatherViewController.swift: The main view controller you use to handle all of the user interactions. This will be the main consumer of the promises.
-
LocationHelper.swift: A wrapper around
CoreLocation
. - WeatherHelper.swift: One final helper used to wrap the weather data provider.
The OpenWeatherMap API
Speaking of weather data, WeatherOrNot uses OpenWeatherMap to source weather information. Like most third-party APIs, this requires a developer API key to access the service. Don’t worry; there is a free tier that is more than generous enough to complete this tutorial.
You’ll need to get an API key for your app. You can get one at http://openweathermap.org/appid. Once you complete the registration, you can find your API key at https://home.openweathermap.org/api_keys.
Copy your API key and paste it into the appID
constant at the top of WeatherHelper.swift.
Trying It Out
Build and run the app. If all has gone well, you should see the current weather in Athens.
Well, maybe… The app actually has a bug (you’ll fix it soon!), so the UI may be a bit slow to show.
Understanding Promises
You already know what a “promise” is in everyday life. For example, you can promise yourself a cold drink when you complete this tutorial. This statement contains an action (“have a cold drink”) which takes place in the future, when an action is complete (“you finish this tutorial”). Programming using promises is similar in that there is an expectation that something will happen in the future when some data is available.
Promises are about managing asynchronicity. Unlike traditional methods, such as callbacks or delegates, you can easily chain promises together to express a sequence of asynchronous actions. Promises are also like operations in that they have an execution life cycle, so you can easily cancel them at will.
When you create a PromiseKit Promise
, you’ll provide your own asynchronous code to be executed. Once your asynchronous work completes, you’ll fulfill your Promise
with a value, which will cause the Promise’s then
block to execute. If you then return another promise from that block, it will execute as well, fulfilled with its own value and so on. If there is an error along the way, an optional catch block will execute instead.
For example, the colloquial promise above, rephrased as a PromiseKit Promise
, looks like:
doThisTutorial()
.then { haveAColdOne() }
.catch { postToForum(error) }
What PromiseKit… Promises
PromiseKit is a Swift implementation of promises. While it’s not the only one, it’s one of the most popular. In addition to providing block-based structures for constructing promises, PromiseKit also includes wrappers for many of the common iOS SDK classes and easy error handling.
To see a promise in action, take a look at the function in BrokenPromise.swift:
func brokenPromise<T>(method: String = #function) -> Promise<T> {
return Promise<T>() { seal in
let err = NSError(
domain: "WeatherOrNot",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "'\(method)' not yet implemented."])
seal.reject(err)
}
}
This returns a new generic Promise
, which is the primary class provided by PromiseKit. Its constructor takes a simple execution block with one parameter, seal
, which supports one of three possible outcomes:
-
seal.fulfill
: Fulfill the promise when the desired value is ready. -
seal.reject
: Reject the promise with an error, if one occurred. -
seal.resolve
: Resolve the promise with either an error or a value. In a way, `fulfill` and `reject` are prettified helpers around `resolve`.
For brokenPromise(method:)
, the code always returns an error. You use this helper function to indicate that there is still work to do as you flesh out the app.
Making Promises
Accessing a remote server is one of the most common asynchronous tasks, and a straightforward network call is a good place to start.
Take a look at getWeatherTheOldFashionedWay(coordinate:completion:)
in WeatherHelper.swift. This method fetches weather data given a latitude, longitude and completion closure.
However, the completion closure executes on both success and failure. This results in a complicated closure since you’ll need code for both error handling and success within it.
Most egregiously, the app handles the data task completion on a background thread, which results in (accidentally) updating the UI in the background! :[
Can promises help, here? Of course!
Add the following right after getWeatherTheOldFashionedWay(coordinate:completion:)
:
func getWeather(
atLatitude latitude: Double,
longitude: Double
) -> Promise<WeatherInfo> {
return Promise { seal in
let urlString = "http://api.openweathermap.org/data/2.5/weather?" +
"lat=\(latitude)&lon=\(longitude)&appid=\(appID)"
let url = URL(string: urlString)!
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data,
let result = try? JSONDecoder().decode(WeatherInfo.self, from: data) else {
let genericError = NSError(
domain: "PromiseKitTutorial",
code: 0,
userInfo: [NSLocalizedDescriptionKey: "Unknown error"])
seal.reject(error ?? genericError)
return
}
seal.fulfill(result)
}.resume()
}
}
This method also uses URLSession
like getWeatherTheOldFashionedWay
does, but instead of taking a completion closure, you wrap your networking in a Promise
.
In the dataTask
‘s completion handler, if you get back a successful JSON response, you decode it into a WeatherInfo
and fulfill
your promise with it.
If you get back an error for your network request, you reject
your promise with that error, falling back to a generic error in case of any other type of failure.
Next, in WeatherViewController.swift, replace handleLocation(city:state:coordinate:)
with the following:
private func handleLocation(
city: String?,
state: String?,
coordinate: CLLocationCoordinate2D
) {
if let city = city,
let state = state {
self.placeLabel.text = "\(city), \(state)"
}
weatherAPI.getWeather(
atLatitude: coordinate.latitude,
longitude: coordinate.longitude)
.done { [weak self] weatherInfo in
self?.updateUI(with: weatherInfo)
}
.catch { [weak self] error in
guard let self = self else { return }
self.tempLabel.text = "--"
self.conditionLabel.text = error.localizedDescription
self.conditionLabel.textColor = errorColor
}
}
Nice! Using a promise is as simple as supplying done
and catch
closures!
This new implementation of handleLocation
is superior to the previous one. First, completion handling is now broken into two easy-to-read closures: done
for success and catch
for errors. Second, by default, PromiseKit executes these closures on the main thread, so there’s no chance of accidentally updating the UI on a background thread.
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!
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.
Where to Go From Here?
You can download the completed version of the project using the Download Materials button at the top or bottom of this tutorial.
You can read the documentation for PromiseKit here.
You may also want to read up on CocoaPods in order to install PromiseKit into your own apps and to keep up-to-date with their changes.
Finally, there are other Swift implementations of promises. One popular alternative is BrightFutures.
If you have any comments, questions or suggestions for alternatives, promise to tell us below! :]
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more