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! Lucky for you, 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 like this:
- 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 handling and success code, which makes it easier to write clean code that handles many different conditions. They work great for complicated, multistep workflows like logging into web services, making authenticated SDK calls, processing and displaying images and more!
Promises are becoming more common and there are multiple solutions, but in this tutorial, you’ll learn about promises using a popular, third-party Swift library called PromiseKit.
Getting Started
The project for this tutorial, WeatherOrNot, is a simple current weather application. It uses OpenWeatherMap for its weather API. The patterns and concepts for accessing this API are translatable to any other web service.
Download the starter project here. PromiseKit is distributed via CocoaPods, but the download already includes the pod. If you haven’t used CocoaPods before and would like to learn about it, you can read our tutorial on it. Other than noting PromiseKit has been installed via CocoaPods, however, this tutorial doesn’t require any other knowledge about CocoaPods.
Open PromiseKitTutorial.xcworkspace, and you’ll see the project is very simple. It only has five .swift
files:
- AppDelegate.swift: This is just the auto-generated app delegate file.
- BrokenPromise.swift: This file creates a placeholder promise that is used to stub some parts of the starter project.
- WeatherViewController.swift: This view controller handles all of the user interaction. This will be the main consumer of the promises.
- LocationHelper.swift: This is a helper file that wraps CoreLocation.
- WeatherHelper.swift: This helper file wraps 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 to run the app. 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 that key and paste it into the appID
constant at the top of WeatherHelper.swift.
Try 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”) to take place in the future when an action is complete (“you finish this tutorial”). Programming promises are similar in that there is an expectation something will be done in the future when some data is delivered.
Promises are about managing asynchronicity. Unlike traditional methods, such as callbacks via completions or selectors, promises can be easily chained together, so a sequence of asynchronous actions can be expressed. Promises are also like operations in that they have an execution lifecycle and can be cancelled.
A PromiseKit Promise
executes a code block that is fulfilled with a value. Upon fulfillment, its then
block is executed. If that block returns a promise, then that will be executed, fulfilled with a value and so on. If there is an error along the way, an optional catch
block will be executed 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>() { fulfill, reject in let err = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "'\(method)' has not been implemented yet."]) reject(err) } } |
This returns a new generic Promise
, which is the primary class provided by PromiseKit. Its constructor takes a simple execution block with two parameters:
fulfill
: A function to call when the desired value is ready to fulfill the promisereject
: A function to call if there is an error
For BrokenPromise
, the code always returns an error. This helper object is used 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(latitude:longitude:completion:)
in WeatherHelper.swift. This method fetches weather data given a latitude, longitude and completion closure.
However, the completion closure is called on either success or failure. This results in a complicated closure since you’ll need code for both error handling and success within it.
Most egregiously, the data task completion is handled on a background thread, so this results in (accidentally :cough:) updating the UI in the background! :[
Can promises help here? Of course!
Add the following right after getWeatherTheOldFashionedWay(latitude:longitude:completion:)
:
func getWeather(latitude: Double, longitude: Double) -> Promise<Weather> { return Promise { fulfill, reject in let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=\(latitude)&lon=\(longitude)" + "&appid=\(appID)" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared let dataTask = session.dataTask(with: request) { data, response, error in if let data = data, let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], let result = Weather(jsonDictionary: json) { fulfill(result) } else if let error = error { reject(error) } else { let error = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown error"]) reject(error) } } dataTask.resume() } } |
This method also uses URLSession
like getWeatherTheOldFashionedWay
does, but instead of taking a completion closure, the networking is wrapped in a Promise
.
In the dataTask
‘s completion handler, if the data is successfully returned, a Weather
object is created from the deserialized JSON. Here the fulfill
function is called with that object, completing the promise.
If there is an error with the network request, that error object is passed to reject
.
Else, if there’s neither JSON data nor an error, then a stub error is passed to reject
as a promise rejection requires an error object.
Next, in WeatherViewController.swift replace handleLocation(city:state:latitude:longitude:)
with the following:
func handleLocation(city: String?, state: String?, latitude: CLLocationDegrees, longitude: CLLocationDegrees) { if let city = city, let state = state { self.placeLabel.text = "\(city), \(state)" } weatherAPI.getWeather(latitude: latitude, longitude: longitude).then { weather -> Void in self.updateUIWithWeather(weather: weather) }.catch { error in self.tempLabel.text = "--" self.conditionLabel.text = error.localizedDescription self.conditionLabel.textColor = errorColor } } |
Nice, using a promise is as simple as supplying then
and catch
blocks!
This new implementation of handleLocation
is superior to the previous one. First, completion handling is now broken into two easy-to-read closures: then
for success and catch
for errors. Second, by default PromiseKit executes these closures on the main thread, so there’s no change of accidentally updating the UI on a background thread.
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.
Replace the new getWeather(latitude:longitude:)
with the following code:
func getWeather(latitude: Double, longitude: Double) -> Promise<Weather> { return Promise { fulfill, reject in let urlString = "http://api.openweathermap.org/data/2.5/weather?lat=" + "\(latitude)&lon=\(longitude)&appid=\(appID)" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared // 1 let dataPromise: URLDataPromise = session.dataTask(with: request) // 2 _ = dataPromise.asDictionary().then { dictionary -> Void in // 3 guard let result = Weather(jsonDictionary: dictionary as! [String : Any]) else { let error = NSError(domain: "PromiseKitTutorial", code: 0, userInfo: [NSLocalizedDescriptionKey: "Unknown error"]) reject(error) return } fulfill(result) // 4 }.catch(execute: reject) } } |
See how easy it is to use PromiseKit wrappers? Here’s the breakdown:
- PromiseKit provides a new overload of
URLSession.dataTask(with:)
that returns aURLDataPromise
, which is just a specializedPromise
. Note the data promise automatically starts its underlying data task. - The returned dataPromise has a convenience method
asDictionary()
, which handles deserializing the JSON for you, significantly reducing the amount of code! - Since the dictionary is already parsed, you use it to create a
result
. You do this usingguard let
to ensure aWeather
object can be created from the dictionary. If not, you create an error and callreject
, similar to before. Otherwise, you callfulfill
with theresult
. - Along the way, the network request could fail, or the resulting JSON deserializing could fail. Before both conditions had to be checked individually. Here, a single
catch
block forwards any errors on through thefail
closure.
In this function, two promises are chained together. The first is the data promise, which returns a Data
from the URL request. The second is asDictionary()
, which takes the data and turns it into a dictionary.
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 that to use the device’s current location.
In WeatherViewController.swift replace updateWithCurrentLocation()
with the following:
private func updateWithCurrentLocation() { // 1 _ = locationHelper.getLocation().then { placemark in self.handleLocation(placemark: placemark) }.catch { error in self.tempLabel.text = "--" self.placeLabel.text = "--" switch error { // 2 case is CLError where (error as! CLError).code == 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 } } } |
- This uses a helper class to work with Core Location. You’ll be implementing it in a moment. The result of
getLocation()
is a promise to get a placemark for the current location. - This catch block demonstrates how different errors are handled within a single catch block. Here a simple
switch
is able 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.promise().then { location in // 2 return self.coder.reverseGeocode(location: location) } } |
This takes advantage of two PromiseKit concepts already discussed: SDK wrapping and chaining.
CLLocationManager.promise()
returns a promise of the current location.- Once the current location is obtained, it’s passed on to
CLGeocoder.reverseGeocode(location:)
, which also returns a promise to provide the reverse-coded location.
With promises, two different asynchronous actions are linked in three lines of code! No explicit error handling is required because that all gets taken care of in the catch
block of the caller.
Build and run. After accepting the location permissions, the current temperature for your (simulated) location is shown. 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 for now about the missing method):
func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() guard let text = textField.text else { return true } _ = locationHelper.searchForPlacemark(text: text).then { placemark -> Void in self.handleLocation(placemark: placemark) } return true } |
This uses the same pattern as all the other promises: go off and find the placemark and when that’s done, update the UI.
Next, add the following to LocationHelper.swift:
func searchForPlacemark(text: String) -> Promise<CLPlacemark> { return CLGeocoder().geocode(text) } |
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 tap Return. This should then go and find the weather for the best match for that name.
Threading
One thing so far has been taken for granted is that all the then
blocks have been executed on the main thread. This is a great feature since most of the work done in the view controller has been to update the UI. Sometimes, however, long-running tasks are best handled on a background thread, so as not to tie up the app.
You’ll next add a weather icon from OpenWeatherMap to illustrate the current weather conditions.
Add the following method to WeatherHelper
right after getWeather(latitude:longitude:)
:
func getIcon(named iconName: String) -> Promise<UIImage> { return Promise { fulfill, fail in let urlString = "http://openweathermap.org/img/w/\(iconName).png" let url = URL(string: urlString)! let request = URLRequest(url: url) let session = URLSession.shared let dataPromise: URLDataPromise = session.dataTask(with: request) let backgroundQ = DispatchQueue.global(qos: .background) _ = dataPromise.then(on: backgroundQ) { data -> Void in let image = UIImage(data: data)! fulfill(image) }.catch(execute: fail) } } |
Here, building a UIImage
from the loaded Data
is handled on a background queue by supplying an optional on
parameter to then(on:execute:)
. PromiseKit then handles the heavy lifting of performing all the necessary dispatches.
Now the promise is fulfilled on the background queue, so the caller will need to make sure the UI is updated on the main queue.
Back in WeatherViewController.swift, replace the call to getWeather(latitude:longitude:)
inside handleLocation(city:state:latitude:longitude:)
with this:
// 1 weatherAPI.getWeather(latitude: latitude, longitude: longitude).then { weather -> Promise<UIImage> in self.updateUIWithWeather(weather: weather) // 2 return self.weatherAPI.getIcon(named: weather.iconName) // 3 }.then(on: DispatchQueue.main) { icon -> Void 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, the
getWeather(latitude:longitude:)
then
block is changed to return aPromise
instead ofVoid
. This means that when thegetWeather
promise is complete, there will be a new promise returned. - The just added
getIcon
method creates a new promise to… get the icon. - A new
then
block is added to the chain, which will be executed on the main queue when thegetIcon
promise is fulfilled.
Thereby, promises can be chained into a sequence of serially executing steps. After one promise fulfills, the next will be executed, and so on until the final then
or an error occurs and the catch
is invoked. The two big advantages of this approach over nested completions are:
- The promises are composed in a single chain, which is easy to read and maintain. Each
then
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. - All the error handling is done in one spot. For example, in a complicated workflow like a user login, a single retry error dialog can be displayed if any step fails.
Build and run, and 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 application. Since there is 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 is finished. This is a common pattern, so PromiseKit has a 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> { // 1 return wrap { // 2 getFile(named: iconName, completion: $0) } .then { image in if image == nil { // 3 return self.getIconFromNetwork(named: iconName) } else { // 4 return Promise(value: image!) } } } |
Here’s how it’s used:
wrap(body:)
takes any function that follows one of several completion handler idioms and wraps it in a promise.getFile(named: completion:)
has a completion parameter of@escaping (UIImage?) -> Void
, which becomes aPromise
. It’s inwrap
‘sbody
block where the original function is called, passing in the completion parameter.- Here, if the icon was not located locally, a promise to fetch it over the network is returned.
- But if the image is available, it is returned in a value promise.
This is a new way of using promises. If a promise created with a value is already fulfilled, it’s then
block will be immediately called. In such, if the image is already loaded and ready to go, it can be returned right away. 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 locally cached value, such as an image here.
To make this work, you’ll have to add 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)! let request = URLRequest(url: url) let session = URLSession.shared let dataPromise: URLDataPromise = session.dataTask(with: request) return dataPromise.then(on: DispatchQueue.global(qos: .background)) { data -> Promise<UIImage> in return firstly { Void in return wrap { self.saveFile(named: iconName, data: data, completion: $0)} }.then { Void -> Promise<UIImage> in let image = UIImage(data: data)! return Promise(value: image) } } } |
This is similar to the previous getIcon(named:)
except that in the dataPromise
‘s then
block, there is a call to saveFile
that is wrapped just like the use ofgetFile
.
This uses a new construct, firstly
. firstly
is functional 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 so that we can be confident that this promise.
All in all, here’s what happens the first time you request an icon:
- First, a URL request is loaded.
- Once that is done, the data is first saved to a file
- After saving, the data is turned into an image and sent down the chain.
If you build and run now, you shouldn’t see any difference in app functionality, but you can check the filesystem to see that the images are saved. To do that, search the console output for the term Saved image to:
. This will show 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 is always taken (like a cleanup task), regardless of success or failure? Well there is: it’s called always
.
In WeatherViewController.swift update handleLocation(city:state:latitude:longitude:)
to show a network activity indicator in the status bar while the weather is being loaded from the server.
Insert the following line before the call to weatherAPI.getWeather...
:
UIApplication.shared.isNetworkActivityIndicatorVisible = true |
Then, to the end of the catch
block add the following:
.always { UIApplication.shared.isNetworkActivityIndicatorVisible = false } |
Then, you might need to assign the whole expression to _
in order to silence an unused result warning.
This is the canonical example of when to use always
. Regardless if the weather is completely loaded or if there is an error, the network activity will be complete, so the activity indicator should always be dismissed. Similarly, this can be used to close sockets, database connections, or disconnect from hardware services.
Timers
One special case is a promise that fulfills, not when some data is ready, but after a certain time interval. Currently, after the weather is loaded, it is never refreshed. Change that to update the weather hourly.
In updateWithCurrentLocation()
, add the following code to the end of the method:
_ = after(interval: oneHour).then { self.updateWithCurrentLocation() } |
.after(interval:)
creates a promise that is fulfilled after the specified interval passes. Unfortunately, this is a one-shot timer. To do the update every hour, it was made recursive onupdateWithCurrentLocation()
.
Parallel Promises
So far, all the promises discussed have either been standalone or chained together in a sequence. PromiseKit also provides functionality for wrangling multiple promises fulfilling in parallel. There are three 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 two functions are when
and join
. Those fulfill after all the specified promises are fulfilled. Where these two differ is in the rejected case. join
always waits for all the promises to complete before rejected if one of them rejects, but when(fulfilled:)
rejects as soon as any one of the promises rejects. There’s also a when(resolved:)
that waits for all the promises to complete, but always calls the then
block and never the catch
.
Note: For all of these grouping functions, all the individual promises will continue until they fulfill or reject, regardless of the behavior of the combining function. For example, if three promises are used in a race
, the race
‘s then
block will be called after the first promise to complete. However, the other two unfilled 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) { let weatherPromises = randomCities.map { weatherAPI.getWeather(latitude: $0.2, longitude: $0.3) } _ = race(promises: weatherPromises).then { weather -> Void in self.placeLabel.text = weather.name self.updateUIWithWeather(weather: weather) self.iconImageView.image = nil } } |
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
block will only be called when the first of those promises are fulfilled. 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 only one is cared about.
Build and run. Once the app is loaded, tap Random Weather.
Updating the condition icon and error handling is left as an exercise for the reader. ;]
Where to Go From Here?
You can download the fully finished sample project here.
You can read the documentation for PromiseKit at http://promisekit.org/, although it is hardly comprehensive. The FAQ http://promisekit.org/faq/ is useful for debugging information.
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, as it is an active pod.
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! :]
The post Getting Started with PromiseKit appeared first on Ray Wenderlich.