From Combine to Async/Await

Migrate APIs without breaking your current code

Eduardo Domene Junior
Geek Culture
3 min readFeb 25, 2022

--

Before Async/Await was officially introduced into Swift, many of us adopted Combine for one-shot operations that don't necessarily need a stream of values, such as API calls. The new feature, however, makes such operations easier to construct and to read, thus it makes sense to consider migrating them. We will see a simple extension that lets us work with both Combine and Async/Await at the same time, making it possible to gradually migrate our code.

Before moving forward, it's important to highlight that Async/Await is not meant to replace Combine. Thus, code that operate on streams should remain using the latter. This article will be focused on code that doesn't necessarily need such capabilities.

Combine-based API

That said, let's see how a simple API call looks like when using Combine.

As you can see, the above API has a single method that fetches a Todo object. Also, it returns an AnyPublisher, that can be subscribed by a client, like so:

Async/Await-based API

In an ideal world, we could simply migrate the previous example to the following Async/Await version:

Take your time to check the differences between the two versions, specially on the client side. As you can see, the new version is much more readable and easier to follow. In addition to that, we don't need to keep a reference to a Cancellable object.

In real life, though, it's much more probable that changing our API to the new version would not only break client1 , but also client2, client3, and so on, making it difficult to tackle all changes at once.

Solution

Working around this problem is actually very simple: we just need to provide a way to extend our Combine API so we could implement Async/Await in individual clients, while still supporting the old API for legacy code, and then, migrate them over time. We could achieve that by implementing the following extension:

The above solution has a problem that is discussed further down in an update. Please check the update section!

Now, we only have to append async() to any AnyPublisher in order to convert it to async:

Breaking the solution down, there are three important points worth mentioning:

1. The function returns Output from AnyPublisher.

Returning the Output means that the function will provide us the same data type used by the Publisher. Also, it will throw the same error raised by the Publisher.

2. Use of withCheckedThrowingContinuation function

This is a function provided by Apple that helps bridging completion-based functions to Async/Await.

3. It calls first() before sinking

By that we guarantee that we terminate the stream after the first value is received, which means that the receiveCompletion block is called and allowing the Cancellable object being deallocated. Calling continuation multiple times will result in a Fatal Error.

Update 07/04

Thanks to Davide de Rosa for pointing out an issue in the previous solution.
It could be possible that the Publisher completes without emitting a value, so it's important to handle that scenario as well. One solution would be to have a flag, checking if indeed it was the case that it finished without emitting any value. The following implementation only differs on the check for the finishedWithoutValue flag:

Conclusion

Migrating from Combine to Async/Await might be tricky in some cases and hard to pull all at once. By converting AnyPublisher to async, we can guarantee backward compatibility while still being able to use the new feature.

Thank you for reading through!

--

--