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.