Published on

Benefiting from synchronous API’s in iOS apps

Waiting for some operation to complete is a common task in every iOS app, making a network request or a query to CoreData are some examples. A way to implement this asynchrony is with the usage of completion handlers (callbacks).

func load(completion: @escaping (Result) -> Void)

In the domain layer I have a protocol that defines how an image (Data) should be loaded, again, a completion handler is used to complete the operation after some work is done:

Old async implementation

But, waiting the time for a task to complete is a threading detail and it should not leak implementations into the domain layer. It may be better to strive for a simpler, synchronous API.

New desired sync implementation

This way, the threading detail is removed from the domain layer and the callback is eliminated (reducing complexity). I’ve also removed the FeedImageLoaderTask because i’m using Combine to handle the asynchronous events (cancelling comes for free).

Main Thread block

Making this operation synchronous means that it is now executed on the main queue, which can block the app’s UI from responding:

Main thread use showing in Xcode

To correct this behaviour we can use receive(on:), a publisher that “affects upstream messages”, to make the operations run in a background execution context. In this case I’m using a separate background queue. The components are thread safe so it can be concurrent as well ✅.

Solution using "receive on" publisher

Type erasure

Just a detail, eraseToAnyScheduler() here is used so I can inject a immediate serial queue when running the acceptance tests, this way the test cases don’t have to wait for the background tasks to complete (they’ll finish faster 🏎). You can check its implementation here.

References

Main repo

Original eraseToAnyScheduler() implementation

Swift Async proposal

receive(on:) doc