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.

Version

  • Swift 4.2, iOS 12, Xcode 10
Update note: Owen Brown updated this tutorial for iOS 12, Xcode 10, Swift 4.2. Michael Katz wrote the original.

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.

OpenWeatherMap API Key

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.

1_build_and_run

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:

  1. 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.
  2. 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:

  1. CLLocationManager.requestLocation() returns a promise of the current location.
  2. 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à!

Build and run, with location

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.

Weather, by search

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:

  1. First, you change the getWeather(atLatitude:longitude:)‘s then block to return a Promise instead of Void. This means that, when the getWeather promise completes, you return a new promise.
  2. You use your just-added getIcon method to create a new promise to get the icon.
  3. You add a new done closure to the chain, which will execute on the main queue when the getIcon promise completes.
Note: You don’t actually need to specify 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:

  1. 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.
  2. 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!

Weather with icon

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:

  1. You construct a Promise much like before, with one minor difference – you use the Promise’s resolve method instead of fulfill and reject. Since getFile(named:completion:)‘s completion closure’s signature matches that of the resolve method, passing down a reference to it will automatically take care of dealing with all resulting cases of the provided completion closure.
  2. 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:

  1. First, make a URLSession request for the icon.
  2. Once that completes, save the data to a file.
  3. 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.

Cached Images

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().

Much, Much Later

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.

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 you use three promises in a 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.

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 at http://promisekit.org/, although it’s 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.

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! :]

Contributors

Comments