Most of the apps we write rely on some sort of data fetching from the network. It can be our backend, or some public API, but we need to get data and we can only to is asynchronously. This is true for networking, but it isn’t the only case. In this article I’ll stick with network requests to build my example.
I’ll show code from one of my GitHub projects https://github.com/gualtierofrigerio/NetworkingExample
How can we deal with asynchronous calls? We have a few options.
We can use a delegate, once the data has been received by our networking class the delegate function is called. That pattern is very common in iOS apps, the first class called in your project is the application delegate, conforming to UIApplicationDelegate. There is nothing really wrong with it, but many times developers like to use block based APIs, so they can make the request and deal with the result on the spot, while having a delegate function takes that responsibility in another function of your class.
So let’s use block based functions. We call our networking class api and provide a callback that will be executed once the data is ready. That’s usually great, until you need to make a few network calls one after another, and you have nested callbacks. Some people call it “callback hell” and I’ll give you an example of that. The third option is using something called a Promise. We make the call to the API and get immediately and object back, then we can observe the object and get notified when our data is ready, or an error occurred. By chaining multiple calls with promises we can avoid nesting too many callbacks and have a much cleaner code. As the title suggests I’m going to focus on callbacks vs promises, so even if I mentioned delegates as another option to deal with asynchronous calls I didn’t implement it on my sample project. There are plenty of examples with delegates to look at, and if you chose to go for it the code is structured in a very different way, so it is harder to compare the three solutions.
Case study
The best way to make a tutorial is writing about a real scenario. For my example I used public APIs you can found here https://jsonplaceholder.typicode.com/
There are a few URLs with JSONs I found perfect for building a quick example.
Let’s say we have a service allowing users to post pictures, grouping them by albums. We have three calls: one for the list of users, one for the albums where each album has a userId and finally a call to retrieve all the pictures, each of them having an albumId.
What we want to do is build a list of users, each of them with their own album, and in each album we want to put all the pictures.
In order to do that we need to make three asynchronous calls, and we can merge all the data when we have the pictures, the albums and the users.
https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/DataSource.swift
As you can see we have 3 structs for Album, Picture and User. I defined two protocols, DataSourceCallbacks for using callbacks and DataSource using promises.
In the same file I put a class for implementing common stuff like merging the structs and decoding data received from the network. The structs are Codable, an alias for Encodable and Decodable, two protocols that allow for easily encode and decode structs from JSONs. All you need to do is call your variables the same way as they’re called into the JSON, or provide a mapping. In my example I named my variables to match the JSON, so JSONDecoder will give me my struct with basically no effort. Cool, isn’t it? Let’s see how it works for Picture
struct Picture:Codable {
var id:Int
var albumId:Int
var title:String
var url:String
var thumbnailUrl:String
}
[
{
"albumId": 1,
"id": 1,
"title": "accusamus beatae ad facilis cum similique qui sunt",
"url": "https://via.placeholder.com/600/92c952",
"thumbnailUrl": "https://via.placeholder.com/150/92c952"
},
{
"albumId": 1,
"id": 2,
"title": "reprehenderit est deserunt velit ipsam",
"url": "https://via.placeholder.com/600/771796",
"thumbnailUrl": "https://via.placeholder.com/150/771796"
},
...
]
as you can see the variables match the JSON, all I had to do was setting the type, so albumId and Id are Int while title, url and thumbnailUrl are String. The following piece of code is responsible for converting a JSON received from the network into one of the Codable structs.
class func decodeData<T>(data:Data, type:T.Type) -> Decodable? where T:Decodable {
let decoder = JSONDecoder()
var decodedData:Decodable?
do {
decodedData = try decoder.decode(type, from: data)
}
catch {
print("decodeData: cannot decode object err \(error)")
}
return decodedData
}
Let me explain what is going on here. The notation <T> means this function can be used with a generic type T, so we can use T to define this type from now on. The function takes the Data parameter containing data from the network and T.Type is the type of object (must be Decodable) we expect to extract from the Data object. In our picture example T.type is [Picture].self so we expect the JSON to be an array of Picture.
Now that we saw how to get structs from JSON is is time to dive into the main topic: callback or promise?
Callback approach
Let’s start with the callback based approach. We ask for the Data object, get it back in a callback, then ask for the next one and once we have all the entities we need we can merge them together and return them to the caller, via a callback of course. I wrote a simple wrapper around URLSession, called RESTClient, you can find here
https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/RESTClient.swift
The protocol has two functions, one with a completion handler and one returning a Promise, let’s focus on the first one.
func getData(atURL url: URL, completion: @escaping (Data?) -> Void) {
let session = URLSession.shared
let task = session.dataTask(with: url) { (data, response, error) in
completion(data)
}
task.resume()
}
The function is pretty simple, it creates a URLSessionDataTask passing a callback, in the form of a closure as last parameter. Once the data is received the completion handler is called. Note I had to put @escaping after the parameter name as the completion handler will be called after the function has returned. All the functions implemented with completion handlers can be found here
https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/DataHandlerCallbacks.swift
And this piece of code retrieves the array of Picture
func getPictures(completion: @escaping ([Picture]?) -> Void) {
getData(forEntity: .Picture, withType:[Picture].self) { (data) in
let pictureData = data as? [Picture]
completion(pictureData)
}
}
private func getData<T>(forEntity entity: Entity, withType type:T.Type, completion:@escaping (Decodable?) ->Void) where T:Decodable {
guard let url = getUrl(forEntity: entity) else {
completion(nil)
return
}
restClient?.getData(atURL: url, completion: { (data) in
guard let data = data else {
completion(nil)
return
}
let decodedData = DataSourceCommon.decodeData(data: data, type:type)
completion(decodedData)
})
}
I already explained what the <T> means, so getData can be called with a generic type and convert the JSON passing this very type to decodeData. As you can see we have completion handlers in multiple places, getPictures has a callback to return the array of Picture, and passes a closure to getData that will have its own closure for the getData function of RESTClient. This approach doesn’t look so bad, we have to implement a closure every time we call one of this function but in the end we either receive the value or nil. Our goal is to merge pictures with albums and users and we need 3 network calls to get all of them, so let’s have a look at the function that takes care of it
func getUsersWithMergedData(completion: @escaping ([User]?) -> Void) {
getUsers { users in
self.getAlbums { albums in
self.getPictures { pictures in
guard let pictures = pictures,
let albums = albums,
let users = users else {
completion(nil)
return
}
let newAlbums = DataSourceCommon.mergeAlbums(albums, withPictures:pictures)
let newUsers = DataSourceCommon.mergeUsers(users, withAlbums: newAlbums)
completion(newUsers)
}
}
}
}
and here is it: callback hell.
getUsers, then getAlbums, then getPictures, and then finally, in the third nested closure, we can check if every call was successful and merge the data before calling the completion handler.
Imagine having a fourth entity, or even more of them. That kind of approach is not ideal at all. I’m sure you could come up with something better, like calling the entities in parallel and then having a way to find out when you have gotten all of them and start the merge, but it wouldn’t look simple, or you could split the guard statement to check for nil values right after each call, and don’t waste time making the others, but it would still require nested callbacks.
Promise approach
Before divining into the code I need to talk about future and promises and how they can help us avoid the nested callback mess I just described.
This is the first time I mention the word future, as I always referred to Promise, even in the title. That’s because I called my implementation Promise, but usually you see questions about futures and promises so I think it is important to mention both of them.
My take on this is a Future is a read only value you can observe as it will be set in the future, while a Promise is something you can either fulfil or reject. So a function can return a Future value, and this value can be observed and eventually either a valid value is set (fulfil or resolve) or an error occurs (reject). Some people have a protocol for Future and an extension of that for a Promise, I chose to implement a class called Future to provide the whole functionality that you can find here https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/Promise.swift
enum PromiseReturn<T> {
case value(T)
case error(Error)
}
Again, <T> is a generic type as we want to use Promise for any kind of value. In our example we deal with Data and Codable, but we could support other classes and structs as well.
The enum has two cases, the value associated with the generic type, so we can set it with the value we’re interested to get from the Promise. The error case contains an Error, so instead of passing a nil value we can be more specific about what went wrong. Note that I could have used a similar approach with callbacks as well, using an enum as return value to pass errors around. I’m showing it here as we can see how to fulfil or resolve (as I called the function) or reject a promise.
private var callbacks = [(PromiseReturn<T>) -> Void]()
private var result:PromiseReturn<T>? {
didSet {
if let res = result {
for callback in callbacks {
callback(res)
}
}
}
Let me start with variables. I know you’re wondering why I have a callbacks array. We were supposed to get rid of callbacks this time, but here they are. As we provide a way to observe the value of a Promise we need to store the callbacks. We could have more than one observer, that’s why there is an array, and in theory we could continue to notify the observers about new values. I say in theory as in our example we make a network call and that’s it, we don’t poll the server for new values, at least not yet.
Next we have the result variable, the value we’re going to return. Once the variable is set we notify all the observer by calling the closures stored in the callbacks array.
func observe(callback: @escaping (PromiseReturn<T>) -> Void) {
callbacks.append(callback)
if let result = result {
callback(result)
}
}
func reject(error:Error) {
result = .error(error)
}
func resolve(value:T) {
result = .value(value)
}
The function observer is the one with the callback, we have a promise object and provide a closure to observer its value changing. The return type is PromiseReturn, as we can report a new value or an error.
So let’s suppose an error occurs, how do we report it back to the observer? We call the reject function, and our result variable is set with an error. That triggers the didSet function, and all the callbacks are fired with that.
Same for the resolve function (when we fulfil the promise), we set the enum to .value and provide a value of the generic type T, then didSet is called and the observers are notified. It isn’t clear at all? Don’t worry, I started to understand the whole concept of a promise once I implemented it. I had to start with the Promise class, but things will get clearer as we saw how a promise is used. What was the reason why we ended up with a callback hell? The network call is asynchronous and requires callbacks, so let’s see how we can change our code by implementing a promise in RESTClient.
func getData(atURL url: URL) -> Promise<Data> {
let promise = Promise<Data>()
let session = URLSession.shared
let task = session.dataTask(with: url) { (data, response, error) in
if let err = error {
promise.reject(error: err)
}
else {
if let data = data {
promise.resolve(value: data)
}
else {
let unknowError = NSError(domain: "", code : 0, userInfo: nil)
promise.reject(error: unknowError)
}
}
}
task.resume()
return promise
}
This getData implementation looks more complex than the previous one, but look at the parameters: there is only one left, and more importantly, there is a return type. The function returns a Promise of type Data, that’s why we don’t need to pass a callback.
A Promise object of type Data is created and returned by the function, and we still need to implement a callback as dataTask requires us to do it, but instead of firing another callback we can deal with the Promise this time. If there is an error, we have to reject the promise and pass that error. If there is data, we can fulfil (resolve) the promise, with the Data value. There is only once piece left, how to observer a promise.
let p = getData()
p.observe { promiseReturn in
switch promiseReturn {
case .value(let data):
// here is our data!
case .error(let error):
// we have an error :(
}
You call getData, assign the promise to a variable and then call observer and provide a closure. Even with promises you eventually need some sort of callback, as you deal with asynchronous calls, but there are two main advantages. The first is you can pass the promise around, and call observer whenever you want. You don’t need to pass a closure to getData, so the code that deals with the value can be elsewhere.
The second advantage is being able to chain multiple operations together, so you call something returning a promise, you pass it to another function that will return another promise and so on.
Let me show what it means, going back to our original goal: getting pictures, albums and users and merge them together. This is how we can achieve that with promises
func getUsersWithMergedData() -> Promise<[User]> {
return getPictures().then({self.addPicturesToAlbums($0)})
.then({self.addAlbumsToUsers($0)})
}
Looks cool to me. We get the pictures, add them to albums, then add the result to the users array and return it as a promise. Like I said before, imagine having more entities to get, just a few more .then calls and you’re done.
You can find the implementation here https://github.com/gualtierofrigerio/NetworkingExample/blob/master/NetworkingExample/DataHandler.swift
but first let’s see what is the function then we call to chain together multiple operations. You can find then in the Promise class
func then<P>(_ block:@escaping(T) -> Promise<P>) -> Promise<P> {
let thenPromise = Promise<P>()
observe { currentPromiseReturn in
switch currentPromiseReturn {
case .value(let val):
let promise = block(val)
promise.observe { result in
switch result {
case .value(let value):
thenPromise.resolve(value: value)
case .error(let err):
thenPromise.reject(error: err)
}
}
case .error(let err):
thenPromise.reject(error: err)
}
}
return thenPromise
}
It looks a little complicated, so let me explain what the function does. We have another generic type P, T is the generic type declared for the class Promise, while P is for that function alone. Why do we need two generic types? Because we can chain operations with multiple values involved. Think about our example, we get a Promise of type Data from the RESTClient then we want this data to become an array of Pictures, so we want a Promise of type [Picture], and after than we retrieve another Data and then convert it to another type and so on.
So the function then takes a closure as parameter, returning a new Promise, and the function itself returns a Promise of the same type. A new promise of type P is created, then observe is called to wait for the current promise to be fulfilled or rejected. The parameter passed to this function returns a promise itself, so we can observe that and finally resolve or reject the new promise of type P we created at the beginning. This is maybe the most complex part of the class, and I hope you can grasp the concept. As I said it wasn’t clear at all to me until I started writing my own implementation and made it work, so I suggest trying it out, for example by taking my Promise.swift file and add to your own project and use it to chain operations together.
There are some good frameworks you can find on GitHub to implement Future and Promise, those are some of them:
https://github.com/freshOS/then
https://github.com/mxcl/PromiseKit
https://github.com/vadymmarkov/When
My example was quite simple, but take a look at some of this project to find out more ways to use promises. For example you can have a .error or .onError functions at the end of multiple .then so you can have a single place to deal with an error, or a .finally called at the end of all the chained operations.
A closing note: I think promises are great when you need to perform multiple operations and chain then, as you can avoid the nested callback hell we saw in the example. You’re still dealing with asynchronous code, and nothing magic is happening, you still have to observe the value and provide a callback eventually, but at least at an higher level the code looks way nicer and clean.