Use Swift’s ‘async’ without breaking the existing codebase

With the release of Swift 5.5 and upcoming release of Xcode 13.2 we are finally able to use new Swift Concurrency APIs.

In this article I will try to show you a way how to use new APIs into the codebase where needed or even required without actually breaking or changing the existing code.

Quick intro to async

One of biggest changes of the new APIs is how we define asynchronous methods. Usually an asynchronous functions is recognised when we see a completion block in its definition.

Let’s say we want to define an ‘UsersNamesLoader’ interface which delivers us an array of strings which represents the names of all our users.

Until now, to make it asynchronous, we needed to define completion block inside of the load method. This resulted into something like in the example bellow.

protocol UsersNamesLoader {
    func load(completion: @escaping (Result<[String], Error>) -> Void)
}

On the client side calling load looks like this:

let loader: UsersNamesLoader
loader.load { result in 
    / * This block gets invoked when result is deliverd */
}

With new Concurrency APIs, we don’t need completion blocks anymore. Now, if we want to define asynchronous operation, we can use a new keyword – async.

If we would try to directly migrate our UsersNamesLoader to new APIs we need to change definition of load method by replacing completion block with async keyword.

protocol UsersNamesLoader {
    func load() async -> Result<[String], Error>
}

But by doing the migration it also forces clients to the UsersNamesLoader to update their code due to our changes.

let loader: UsersNamesLoader
let result = await loader.load()
/* Print method gets invoked after load method delivers result */
print(result)

We did a quick glimpse over new async keyword and how it influences our clients. To learn more about new Concurrency API I suggest reading Swift’s documentation.

Motivation

With SwiftUI, Apple is actually forcing us to use new APIs as well. One of the challenges that I had recently was to use new refreshable(action:) instance method on SwiftUI’s View.

func refreshable(action: @escaping () async -> Void) -> some View

Looking at the API closely, you can see that, with calling of the refreshable, we need to provide an action which is defined by using async keyword.

This brings us into situation where we need to decide how to approach this requirement. In introduction part I have shown you how to convert existing code to the new API. So one of the solutions would be to simply change the load method to the new API.

But imagine, that you are working existing project and there is a bunch of other clients of UsersNamesLoader and their codebase and usage of the loader is not ready to adopt new APIs. Or even worse, what if loader is actually a third party framework.

Solution

Thankfully Apple provided an API – ‘withCheckedContinuation’ to tackle this challenge. Say hello to AsyncAwaitWrapper. Let’s dive in.

class AsyncAwaitWrapper<Resource, ResourceError> where ResourceError: Error {
    typealias ResourceLoader = (_ completion: @escaping (Result<Resource, ResourceError>) -> Void) -> Void
    let loader: ResourceLoader
    
    init(loader: @escaping ResourceLoader) {
        self.loader = loader
    }
    
    func load() async -> Result<Resource, ResourceError> {
        return await withCheckedContinuation { continuation in
            loader() { result in
                continuation.resume(returning: result)
            }
        }
    }
}

AsyncAwaitWrapper is a generic class which accepts ResourceLoader upon initialisation. If we look closely to definition of ResourceLoader we can see that signature matches the signature of the load method in UsersNamesLoader. By making it generic we can reuse it with other loaders as well.

But the real star here is the load method. You can quickly notice that it implements new async keyword. To use and map execution of loader to new load method we can use withCheckedContinuation from CheckedContinuation API.

And how does it look in usage?

let loader: UsersNameLoader = ...
let asyncLoader = AsyncAwaitWrapper(loader: loader.load)
let result = await asyncLoader.load()

Finally we can use our loader with refreshable easily and without changing the existing code which was the main gol from beginning.

.refreshable {
    let _ = await asyncLoader.load() 
}

Conclusion

And that’s it. We made quick and simple wrapper around our existing code to use it in places where it requires usages of new API – async in our example.

If you are interested in actual code backed up with Unit Tests I invite you to check it on GitHub as well.

In case of any questions or comments feel free to contact me or leave it in the comments section bellow.

Architectural Patterns – Decorator

/

In this article I will present you powerful architectural pattern that I have been using it a lot called Decorator.

1. What is Decorator

...

Detect user’s country without accessing their location

/

Sometimes we are faced with challenge where we would like to improve user experience of our app based on where in the world or better said in which country ...

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments