The issue with callbacks

After our thorough review of closures, it is time to take a step back and reconsider our original goal for this chapter: showing effective ways to handle concurrency in Swift. We have learned that callbacks provide an architectural building block to write concurrent code, and that closures are a great way to implement callbacks. In this section, we should ask ourselves how callbacks perform in more complex, realistic scenarios.

We have already met one shortcoming of using callbacks when we implement them through closures, namely, the risk of retain cycles. But there are more fundamental issues with callbacks in general, as we will shortly see.

Let's consider this scenario:

Using callbacks, you could think of writing the following pseudo-code:

getListOfItems(listUrl) { list in
list.forEach { item in
getImage(url: item.url) { image in
if let image = image {
viewModel.addItem(item.categoryName, image)
} else {
getImage(url: item.alternateUrl) { image in
if let image = image {
viewModel.addItem(item.categoryName, image)
} else {
viewModel.addItem(item.categoryName,
defaultImage(category: item.categoryName))
}
}
}
}
}

Although it solves a pretty trivial problem, this code has some big issues:

The preceding example shows a general property of code that uses callbacks to handle sequences of asynchronous operations when each async operation depends on the result of the previous. It is so widespread that it even has a name: callback hell.

Of course, we could rewrite our code using functions to improve its modularity, as in the following example:

func useImage(name: String, url: String, onError: () -> Void) {
getImage(url: url) { image in
if let image = image {
viewModel.addItem(name, image)
} else {
onError()
}
}
}

getListOfItems(listUrl) { list in
list.forEach { item in
useImage(item.categoryName, item.url) {
useImage(item.categoryName, item.alternateUrl) {
viewModel.addItem(item.categoryName,
defaultImage(category: item.categoryName))
}
}
}
}

As you can see, while nesting has not improved, at least we could write code that expresses more clearly our aim and with less code duplication. This is still far from being a good solution, though, and, indeed, on two accounts:

In the rest of this chapter, we will examine two alternative approaches to deal with concurrent operations, namely futures and promises, and reactive programming, which leverages callbacks with the aim of providing higher-level abstractions to handle concurrency.