Upcoming and OnDemand Webinars View full list

Getting Started with Deferred

Amit Bijlani

At Big Nerd Ranch, we have created a library called Deferred, which makes it easier for developers to work with data that is not yet available but will be at some point in the future. For example, when you’re downloading data from the network or parsing data, the data is not yet available because some work needs to be completed first. You might know this concept as Futures or Promises, but if not, don’t worry—this post will help you gain a better understanding.

Status Quo

Most iOS apps have to deal with asynchronous tasks, such as networking or some kind of processing. Every iOS developer knows better than to block the main UI with some time-consuming task. We do this by adopting either the delegation pattern or the completion handler pattern.

Delegation

protocol DownloaderDelegate: class {
    func downloadDidFinish(with data: Data?, by downloader: Downloader)
}

This pattern works fine when our application is simple, but as the complexity increases, we’re driven to coordinate multiple delegates, which gets harder and more error-prone as delegate callbacks continue to multiply.

Completion Handler

You say that delegation is an old pattern and I use completion handlers (callbacks), which is fantastic. Instead of the delegate example from the last section, you’d write something like this:

func download(contentsOf url: URL, completion: @escaping (Data?) -> Void)

This is a lot better because you are capturing scope and passing a closure. It’s modern code that is easier to read. But how do you manage multiple asynchronous blocks of code that are dependent on each other? You end up with nested callbacks.

That’s an acceptable solution at first. You say you just need to download an image? The completion handler works great. However, in the long run, when you need to download an image, turn it into a thumbnail if it’s bigger than a certain size, rotate it, and run it through a filter to increase contrast, well, suddenly it gets a lot more painful. You end up with a rat’s nest of logic along with a pyramid of doom:

// nested callbacks or pyramid of doom

download(contentsOf: url) { data in
  let	downloadedImage = ...
	
  downloadedImage.resizeTo(...) { resizedImage in
    resizedImage.rotateTo(...) { rotatedImage in
      rotatedImage.applyFilter(...) { finalImage in
        // so something with the final image
      }
    }    
  }
}

Introducing Deferred

Asynchronous programming involves waiting for a value. This value could be downloaded from the network, loaded form the disk, queried from a database or calculated as a result of a computation. Before we can pass this value around, we need to wait till it exists. This is where we get into the issue of nested callbacks or completion handlers, which are useful, but not the most effective style of programming, because as the code complexity increases, it degrades the readability of the code.

A Synchronous Starting Point

What if we were able to pass around a value without having to worry about its existence? Let’s say we needed to download data from the network and display it in a list. We first need to download that data, parse it and then ready it for display:

let items = retrieveItems() 
datasource.array = items
tableView.reloadData()

An Asynchronous Version

The above code represents a very basic version. It relies on a synchronous retrieveItems call that will block your UI until data is available. We can write an asynchronous version of the same code which would look something like the following:


retrieveItems { items in
	DispatchQueue.main.async() {
		 datasource.array = items
		 tableView.reloadData()   
	}
}

In both the sync and async cases, we have to explicitly call the method to retrieve items and we cannot pass around items until we have a value. In essence, the array of items is something that will exist in the future, and the method retrieveItems promises to provide it to us.

Rewriting with Deferred

Deferred uses the idea of futures and promises to defer providing a value. Since we are dealing with Swift, these concepts map easily to the type system, where we can model both a future and a promise with generics:

  • Future<T> – read-only placeholder for a value of type T
  • Promise<T> – write-once container for a value of type T

Let’s try recreating our previous example using Deferred:

import Deferred

let deferredItems = Deferred<[Item]>()

Here we are simply saying that we are creating a type Deferred which will provide us with an array of items. The great thing about Deferred is that you can chain one or many calls to upon each of whose closure argument will execute once we have a value:

import Deferred

let deferredItems: Deferred<[Item]>()
deferredItems.upon(DispatchQueue.main) { items in
	datasource.array = items
	tableView.reloadData()
}

The upon method’s first argument determines where the closure will execute once the value arrives. Here, we’ve aimed it at the main queue. When displaying the items we are only concerned with what we are going to do once the items are available. The closures is not worried about how or when to retrieve the items or where the retrieval should execute.

Elsewhere in your app you can retrieve those items and then fill them in your Deferred object.

deferredItems.fill(with: retrieveItems())

The above code is focused on providing a value to your deferredItems. You could easily make retrieveItems asynchronous with a completion handler filling in the value:

//Asynchronous example
retrieveItems { items in 
	deferredItems.fill(with: items) 
}

upon tells Deferred that the closure is waiting to run whenever it gets filled. When a Deferred gets filled with a value, all the waiting closures get called with that value. (And if it never gets filled, they never run.)

Result Type

The above example does not take errors into consideration, but anything can go wrong when making a network request. So how do you handle errors with Deferred? Remember that Deferred represents a value of any type. If there’s no chance of error, you’d use a Deferred<Value> to say that you expect to eventually deliver a value. But very few things are certain, so to represent the possibility of eventually discovering either a value or an error, we typically use the Result type:

enum Result<SuccessValue> {

    /// Contains the value it succeeded with
    case success(SuccessValue)

    /// Contains an error value explaining the failure
    case failure(Error)
}

The same example using a Result type would look like the following:

import Deferred 

let deferredItems = Deferred<Result<[Item]>>

deferredItems.upon(.main) { result in
    switch result {
    case let .success(value):
       datasource.array = value
       tableView.reloadData()
       
    case let .failure(error):
        print(error)
    }
}

You don’t have to create your own Result type because the Deferred framework provides you with one.

Note that in the above example, we use .main when passing a parameter to upon, instead of DispatchQueue.main since DispatchQueue is implied. Either is acceptable, but the latter makes your code more readable.

Learn More About Deferred

There is more you can do with Deferred like chaining async calls, or running a closure till several Deferreds have been filled. It even has a Task type for reporting progress or cancelling a task. To find out more, check out the documentation or—even better!—attend one of our Advanced iOS bootcamps.

Not Happy with Your Current App, or Digital Product?

Submit your event

Let's Discuss Your Project

Let's Discuss Your Project