Contact Tracing: 7 Ways Google and Apple are Protecting User Privacy
Android iOSContact tracing apps are already being built without a standard of privacy set; regardless of the efficacy of contact-tracing as a solution, there's a...
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.
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.
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.
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
}
}
}
}
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.
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()
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.
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.)
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.
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.
Contact tracing apps are already being built without a standard of privacy set; regardless of the efficacy of contact-tracing as a solution, there's a...
The best user interfaces are interactive and responsive to the user. UIKit is a capable framework that lets you build apps that users expect,...