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.

Leave a rating/review
Download materials
Save for later
Share
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.