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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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