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 3 of 4 of this article. Click here to view the first page.

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