Using PromiseKit
Dejan Agostini

Working in swift (and ObjC) you should be comfortable working with async code. Sooner or later you'll run into a problem called 'callback hell' where in the completion handler of one method you're doing another async call. And in the completion handler of that async method you call another, and so on... 'Callback hell' makes your code hard to follow and less elegant. PromiseKit is a framework that will simplify working with async code and make your code very elegant and simple to follow. This pattern is not specific for PromiseKit, in fact, promises can be found in many languages. In one of the previous articles we used promises in JavaScript when we talked about Firebase Cloud Functions. PromiseKit is a swift implementation of the promises. In this article we'll go over some most common examples and get you started with PromiseKit.
Promises
Promises are wrappers around your async tasks. They can succeed or fail. Instead of using closures in your functions, your functions simply return a 'Promise' object. Take a look at the example of a function before and after using PromiseKit:// Before PromiseKit
func getRandomImage(onCompleted: @escaping ((Image?, Error?)->())) {/*...*/}
// After PromiseKit
func getRandomImage() -> Promise<Image> {/*...*/}
You can notice the difference in readability already. The power of promises is that they can be chained. We'll talk about that in a bit.
Guarantees
A 'Promise' can be rejected, but a 'Guarantee' always succeeds. That is the main difference between them. A good example of a 'Guarantee' are animations. Guarantees were introduced in PromiseKit 5 in order for you to be able to write code without writing the error handling logic.Promise Chain
Promises are linked into a chain, and this makes your code incredibly readable. You also won't get the 'Callback hell' that you might encounter in your standard async code. This is how a typical promise chain looks like:func promiseChain() {
firstly {
self.fetchData()
}.then { data in
self.processData(data)
}.done { (value) in
print("did get value: \(value)")
}.catch { (error) in
print("got an error: \(error)")
}
}
func fetchData() -> Promise<String> {
return Promise { seal in
seal.resolve(.fulfilled("did fetch data"))
}
}
func processData(_ data: String) -> Promise<Int> {
print("processing data: \(data)")
return Promise { seal in
seal.resolve(.fulfilled(42))
}
}
You start your chain with the 'firstly' block. In this block you execute a function that returns a 'Promise'. 'firstly' is just syntactic sugar and you can easily call the function directly. When the first block is finished executing and the 'Promise' is resolved, the next block in the chain gets executed. In our case it's the 'then' block. In this block you have access to the resolved value of the previous block. The promise was using a 'String' as a type parameter so the value that will be passed to the 'then' block will be a string. Notice how this value is not an optional. The 'then' block will take in values from the previous 'Promise' and will return another 'Promise'. You can see how we can easily chain a lot of promises together.
'done' block is similar to the 'then' block. The only difference is that the 'done' block is not returning a promise. This is supposed to be the end of the chain. All the errors are handled in the 'catch' block. If any of the blocks in the chain failed. To be precise, if any of the promises got rejected. All the subsequent blocks would be skipped and the 'catch' block would be called. In our code example we'll see that there's one type of block that always executes, the 'ensure' block. More on that later.
These are the basics of PromiseKit. Next we'll implement a simple app where you'll learn how to use this in practice.
An Example
We'll build a simple app that will connect to unsplash.com, fetch a random photo, parse the meta-data and display the photo on the screen. This is a common scenario you'll find in a lot of apps and we'll see how simple it is to do it using PromiseKit.Project Prep
Let's quickly go over some assumptions. Obviously, you'll need your API key to connect to unsplash. You can register for a free dev account on unspash.com. We'll only be using the endpoint for getting random photos. To keep this article focused on PromiseKit I won't be going through some utility classes; like, parsing the response from the server into objects. I'll assume you know how to do that. You can always checkout the example project and look through that code. So, let's start making promises :)Making Promises
In the simple example above, we had a function that was returning a 'Promise' object. We'll create two functions. One will make an API call and return image metadata. The other one will return the actual image data.Fetch and Parse
The first function is a very common pattern in iOS development, fetch and parse data. Let's check it out:func getRandomImage() -> Promise<Image> {
let requestString: String = "\(config.Host)\(config.Endpoint)?client_id=\(config.APIKey)"
guard let url = URL(string: requestString) else {
return Promise(error: USError.CannotCreateURL)
}
return
firstly {
self.session.dataTask(.promise, with: url)
}.compactMap(on: self.queue) { (data, response) -> [String : Any]? in
try JSONSerialization.jsonObject(with: data, options: []) as? [String : Any]
}.compactMap(on: self.queue) { (imageDict) -> Image? in
self.factory.getImage(fromDict: imageDict)
}
}
In the guard statement you can see that we're returning a promise that rejects with an error. We can define our custom errors here, like we usually would. If this promise was ever returned, our promise chain would stop executing and the 'catch' block in the chain would be executed.
PromiseKit has a lot of extensions for most of the UIKit and Foundation. For example, URLSession has an extension where you can wrap your standard data task into a promise. The wrapped data task will be resumed automatically. When the task completes, the results will be passed in to the next block. Which in our case is the 'compactMap'.
A thing to note is that we're executing our 'compactMap' block in the background. You can set the execution queue on most blocks in PromiseKit.
This 'compactMap' block is similar to the 'compactMap' that you're used to. This block can return an optional. If your object is nil, the block will emit an error and your chain will break. If your object is not nil, it will be wrapped in a promise and returned to the next block. The error that this block returns will be a generic PromiseKit error. If you want to return your custom error you can easily do so. Just return a promise that rejects with your custom error, like we did a couple of lines above.
We also have 'map' at our disposal and it's doing exactly what you would expect :)
Download Data
The second function that we'll cover is also one that's commonly found in iOS development. Downloading large files. This is a fairly simple function. So we'll use the opportunity to demonstrate how to pass multiple values out from our function:func getPhotoData(_ image: Image) -> Promise<(Image, Data)> {
return
firstly {
self.session.dataTask(.promise, with: image.imageURL)
}.then {(data, response) -> Promise<(Image, Data)> in
.value((image, data))
}
}
We're passing the 'Image' object to the function, but we're returning a promise that contains a tuple. Tuples are a simple way to return multiple values from your function. You'll notice that we're using a convenience function '.value(..)' to create our promise.
Guaranteed Animation
After making some promises, we can make some guarantees. A guarantee is a promise that can't fail. A good example would be animations. So we'll create our animations guarantee:private func startLoading() -> Guarantee<Bool> {
self.activityIndicator.startAnimating()
self.imageView.isUserInteractionEnabled = false
return UIView.animate(.promise, duration: 0.25) {
self.imageView.alpha = 0.5
}
}
This function will start our activity indicator, disable user interaction on the image view and return an animation guarantee using the UIView extension.
Let's see how all these bits fit together.
Putting it Together
What we'll do is start the loading animations, fetch a random photo, get the raw image data, convert the data to UIImage, update the UI, stop the loading animations and display any errors to the user. If you had to code all this without using PromiseKit, you would write quite a few lines of code, but with PromiseKit it's only a couple of lines of code. This is the function that's doing all the work:private func getRandomPhoto() {
firstly {
self.startLoading()
}.then { _ in
self.controller.getRandomImage()
}.then { (image) in
self.controller.getPhotoData(image)
}.then(on: DispatchQueue.global(qos: .userInitiated)) { (image, data) -> Promise<(Image, UIImage)> in
Promise<(Image, UIImage)> { seal in
guard let uiImage = UIImage(data: data) else {
seal.reject(VCError.cantCreateUIImage)
return
}
seal.fulfill((image, uiImage))
}
}.done { (result) in
self.imageTitle.text = result.0.description
self.imageView.image = result.1
}.ensure {
self.stopLoading()
}.catch { error in
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Dismiss", style: .default, handler: nil))
self.present(alert, animated: true, completion: nil)
}
}
If you look at it more closely it's doing all the steps that we've just described and it's reading nicely from top to bottom. No more callback hell :) A few notes about the function. We're creating a UIImage in one of the blocks and returning a promise that rejects with a custom error, if the image creation fails. The 'ensure' block will always execute, no matter what. This is the perfect place to stop all the loading animations.