Articles, podcasts and news about Swift development, by John Sundell.

Using tokens to handle async Swift code

Published on 29 Oct 2017
Discover page available: Concurrency

Handling asynchronous code in a way that is predictable, readable and easy to debug can be really challenging. It's also something that is very common, almost all modern codebases have parts that are highly asynchronous - whether that's loading data over the network, processing big datasets locally or any other computationally intensive operations.

It's therefore not surprising that in most languages, several abstractions have been created to try to make dealing with asynchronous tasks easier. We've already taken a look at a few such techniques and abstractions in previous posts - including using GCD and Futures & Promises. This week, let's take a look at another technique that can make managing asynchronous calls a bit simpler and less error prone - using tokens.

Token-based APIs

The core idea of token-based APIs is that a function that starts an asynchronous or delayed operation returns a token. That token can then be used to manage and cancel that operation. The signature of such a function can look like this:

func loadData(from url: URL, completion: @escaping (Result) -> Void) -> Token

Tokens are much more lightweight than, for example, Futures & Promises - since the token simply acts as a way to keep track of a request, rather than containing the entire request itself. This also makes them a lot easier to add to an existing code base, rather than having to rewrite a lot of asynchronous code using a different pattern, or having to expose implementation details.

So what's the benefit of having a function like the above return a token? Let's take a look at an example, in which we are building a view controller to allow a user to search for other users in our app:

class UserSearchViewController: UIViewController, UISearchBarDelegate {
    private let userLoader: UserLoader
    private lazy var searchBar = UISearchBar()

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        userLoader.loadUsers(matching: searchText) { [weak self] result in
            switch result {
            case .success(let users):
                self?.reloadResultsView(with: users)
            case .error(let error):
                self?.handle(error)
            }
        }
    }
}

As you can see above, this all looks pretty standard, but if you have ever implemented this kind of search-as-you-type feature before - you probably know there's a bug hidden in there. The problem is that the speed of which characters are typed into the search field won't necessarily match how quickly requests are completed. This leads to a very common problem of accidentally rendering old results when a previous request will finish after a subsequent one.

Let's take a look at how tokens can help us solve the above problem 👍

A token for every request

Let's start by taking a closer look at UserLoader. It's currently implemented like this:

class UserLoader {
    private let urlSession: URLSession

    func loadUsers(matching query: String,
                   completionHandler: @escaping (Result<[User]>) -> Void) {
        let url = requestURL(forQuery: query)

        let task = urlSession.dataTask(with: url) { (data, _, error) in
            // Pattern match on the returned data and error to determine
            // the outcome of the operation
            switch (data, error) {
            case (_, let error?):
                completionHandler(.error(error))
            case (let data?, _):
                do {
                    let users: [User] = try unbox(data: data)
                    completionHandler(.success(users))
                } catch {
                    completionHandler(.error(error))
                }
            case (nil, nil):
                completionHandler(.error(Error.missingData))
            }
        }

        task.resume()
    }
}

What we need to do in order to avoid bugs in our UserSearchViewController, is to enable any ongoing task to be cancelled. There are a couple of ways that we could achieve that. One option is to simply have UserLoader itself keep track of its current data task, and cancel it whenever a new one is started:

class UserLoader {
    private let urlSession: URLSession
    private weak var currentTask: URLSessionDataTask?

    func loadUsers(matching query: String,
                   completionHandler: @escaping (Result<[User]>) -> Void) {
        currentTask?.cancel()

        let url = requestURL(forQuery: query)

        let task = urlSession.dataTask(with: url) { (data, _, error) in
            ...
        }

        task.resume()
        currentTask = task
    }
}

The above works, and is a totally valid approach. However, it does make the API a bit less obvious - since cancellation is now mandatory and baked into the implementation. That could potentially cause bugs in the future if UserLoader is used in another context in which previous requests shouldn't be cancelled.

Another option, is to return the data task itself when loadUsers is called:

class UserLoader {
    private let urlSession: URLSession

    func loadUsers(matching query: String,
                   completionHandler: @escaping (Result<[User]>) -> Void) -> URLSessionDataTask {
        let url = requestURL(forQuery: query)

        let task = urlSession.dataTask(with: url) { (data, _, error) in
           ...
        }

        task.resume()

        return task
    }
}

Also a valid approach, but the downside here is that we are leaking implementation details as part of the API. There's no reason (other than allowing cancellation) that other code using UserLoader should have to know that it uses URLSessionDataTask under the hood - that should ideally be kept a private implementation detail to make things like testing & refactoring easier.

Finally, let's try solving this using a token-based API instead. To do that, we're first going to create a token type that we'll later return when a request is performed:

class RequestToken {
    private weak var task: URLSessionDataTask?

    init(task: URLSessionDataTask) {
        self.task = task
    }

    func cancel() {
        task?.cancel()
    }
}

As you can see above, our RequestToken is super simple, and really just contains a reference to the task that can be cancelled. There's really no reason for it to be more complicated, and it's totally fine for it to hold a reference to the task itself, as long as we don't expose it publicly.

Let's go ahead and use RequestToken as the return type in UserLoader:

class UserLoader {
    private let urlSession: URLSession

    @discardableResult
    func loadUsers(matching query: String,
                   completionHandler: @escaping (Result<[User]>) -> Void) -> RequestToken {
        let url = requestURL(forQuery: query)

        let task = urlSession.dataTask(with: url) { (data, _, error) in
            ...
        }

        task.resume()

        return RequestToken(task: task)
    }
}

Note that we also add the @discardableResult attribute to the method, so that we don't force the API user to use the token in case it's not needed.

The result is a neatly abstracted API that enables cancellation without introducing additional state - pretty sweet! 🍭

Using a token to cancel an ongoing task

Time for the final piece of the puzzle - actually using the token to cancel any previously unfinished request as soon as the user types a new character in our view controller's search bar.

To do that, we're going to add a property for storing the current request token, and before we start a new request we simply call cancel() on it. Finally, since we have introduced cancellation, we might want to update our result handling code to deal with URLError.cancelled specifically, so that we don't end up displaying an error view whenever we cancel an old request.

Here's what the final implementation looks like:

class UserSearchViewController: UIViewController, UISearchBarDelegate {
    private let userLoader: UserLoader
    private lazy var searchBar = UISearchBar()
    private var requestToken: RequestToken?

    func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        requestToken?.cancel()

        requestToken = userLoader.loadUsers(matching: searchText) { [weak self] result in
            switch result {
            case .success(let users):
                self?.reloadResultsView(with: users)
            case .error(URLError.cancelled):
                // Request was cancelled, no need to do any handling
                break
            case .error(let error):
                self?.handle(error)
            }
        }
    }
}

Conclusion

Adding tokens to your existing asynchronous APIs can be a great way to easily add cancellation support, without having to completely rewrite your implementation. While other, more sophisticated async abstractions (such as Futures/Promises, RxSwift, Operations, etc) provide a lot more powerful features than simple tokens can offer - if all you need is cancellation, tokens can be a great option.

We could also go one step further, and make our RequestToken more generalized, by having it take an object conforming to a Cancellable protocol instead of a concrete URLSessionDataTask object. But in general I'd suggest keeping tokens super simple, to avoid having them grow into something that looks more like a Future/Promise.

What do you think? Have you used tokens to manage async operations in the past, or is it something you'd like to try out in the future? Let me know, along with any questions, comments or feedback that you might have - on Twitter @johnsundell.

Thanks for reading! 🚀