Avoid Pyramids of Doom With PromiseKit

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ł!