Avoid Pyramids of Doom With PromiseKit

Photo of Piotr Sochalewski

Piotr Sochalewski

Aug 14, 2018 • 5 min read
pyramids
In computer programming, the pyramid of doom is a common problem that arises when a program uses many levels of nested indentation to control access to a function. It is commonly seen when checking for null pointers or handling callbacks.

This quote from Wikipedia simply explains what pyramid of doom is. It's really easy in modern programming languages to encounter the issue, especially when executing asynchronous code.

Futures and promises are designed to resolve this problem. Some languages offer native solutions, but when it comes to Obj-C and Swift there is a pretty cool third-party library that allows handling it easily: PromiseKit.

Its syntax is similar to reactive programming patterns, such as RxSwift. To be honest I'm pretty sure that using both RxSwift and PromiseKit at the same time is pointless.

Promise<T>

PromiseKit uses a generic Promise class to represent asynchronous tasks. They are only as useful as tasks they represent. At the moment, almost all Apple's APIs are converted to promises. You can find all official extensions here.

When you use them, you don't have to write your own promises in most cases, but in case you actually need to write one, it will be really simple!

func promise(after interval: TimeInterval) -> Promise<Void> {
    return Promise<Void> { seal in
        DispatchQueue.main.asyncAfter(deadline: .now() + interval) {
            seal.fulfill(())
        }
    }
}

seal is a generic class Resolver<T> instance, which has to be fulfilled (when it succeeds) or rejected passing an error (when it fails). Pretty easy, huh?

PS. The example above should be a Guarantee<T>, because it never fails, but let's stay with Promise for now. In short, Guarantee is a Promise that always succeeds.

Usage

Executing promises is based on closures. Let's take a look at the example below:

UIApplication.shared.isNetworkActivityIndicatorVisible = true

promise(after: 5.0)
    .map { "It's 5 seconds later" }
    .done { print($0) }
    .ensure { UIApplication.shared.isNetworkActivityIndicatorVisible = false }
    .catch { error in print("Oopsies! \(error.localizedDescription)") }

This one shows a network activity indicator at first, then calls the promise above. It is converted inside the map block to a String and then printed to the debug console if the promise is fulfilled. After this, the ensure block is called (it doesn't matter whether the promise is fulfilled or rejected) to hide an activity indicator. If the promise fails (this particular one cannot fail, as it's always fulfilled and never rejected), the error's localised description will be printed.

Let's build something useful!

It's hard to say that the promise above is useful. Don't waste time and create a new project with PromiseKit and PromiseKit/CoreLocation in your Podfile.

We're going to build a simple app that gets your current location, sends a request for the current weather and shows the temperature, and then gets an icon with your current weather conditions.

To make it with standard API, you need at least a hundred lines of code, but with PromiseKit a dozen and a half would be enough.

First, let's create the properties for UI elements. We need two labels and one image view. I prefer storyboards for such small projects, so here goes my IBOutlets.

@IBOutlet private weak var cityLabel: UILabel!
@IBOutlet private weak var weatherImageView: UIImageView!
@IBOutlet private weak var temperatureLabel: UILabel!

Then add two simple functions that help generate URL for requests.

private func url(coordinate: CLLocationCoordinate2D) -> URL {
    return URL(string: "https://api.openweathermap.org/data/2.5/weather?lat=\(coordinate.latitude)&lon=\(coordinate.longitude)&units=metric&APPID=[YOUR_APP_ID]")!
}
    
private func url(icon: String) -> URL {
    return URL(string: "https://openweathermap.org/img/w/\(icon).png")!
}

Please remember to replace [YOUR_APP_ID] with your real OpenWeatherMap API key. You can read how to get one here.

Then add these 3 structs that conform to Codable to easily convert the received JSON response to objects.

struct OpenWeatherMapResponse: Codable {
    let weather: [WeatherResponse]
    let main: MainResponse
    let name: String
}

struct WeatherResponse: Codable {
    let main: String
    let icon: String
}

struct MainResponse: Codable {
    private let temp: Double
    
    var temperatureInCelsius: String {
        return "\(Int(temp))°"
    }
}

Last, but not least: add the code below to viewDidLoad() and that would be it!

CLLocationManager.requestLocation()
    .lastValue
    .map { $0.coordinate }
    .map { self.url(coordinate: $0) }
    .then { URLSession.shared.dataTask(.promise, with: $0) }
    .compactMap { try JSONDecoder().decode(OpenWeatherMapResponse.self, from: $0.data) }
    .get {
        self.temperatureLabel.text = $0.main.temperatureInCelsius
        self.cityLabel.text = $0.name
    }
    .map { return $0.weather.first!.icon }
    .map { self.url(icon: $0) }
    .then { URLSession.shared.dataTask(.promise, with: $0) }
    .compactMap { UIImage(data: $0.data) }
    .done { self.weatherImageView.image = $0 }
    .ensure { UIApplication.shared.isNetworkActivityIndicatorVisible = false }
    .catch { print($0.localizedDescription) }

One of the best parts of PromiseKit is the code that is easy to read, almost like a book. This makes it self-explanatory.

More examples

You can find more examples of PromiseKit usage on my Netguru colleague's GitHub repository. Thank you, Michał Warchał!

Tags

Photo of Piotr Sochalewski

More posts by this author

Piotr Sochalewski

Piotr's programming journey started around 2003 with simple Delphi/Pascal apps. He has loved it...
How to build products fast?  We've just answered the question in our Digital Acceleration Editorial  Sign up to get access

We're Netguru!

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency
Let's talk business!

Trusted by:

  • Vector-5
  • Babbel logo
  • Merc logo
  • Ikea logo
  • Volkswagen logo
  • UBS_Home