Categories
iOS Swift SwiftUI

Wrapping a long-running function with async-await in Swift

In this blog post, we are going to take a look at a case where we have a long-running synchronous function, and we want to wrap this with an async version. The aim is that the long-running function runs on a background thread instead and keeps the UI snappy. We’ll be using structured concurrency features. In the example code, we’ll also add print statements to see what thread the code is running on.

struct ImageProcessor {
func syncProcessData(_ imageData: Data) -> UIImage {
print(#function, "started", Thread.isMainThread, Thread.current)
Thread.sleep(forTimeInterval: 10) // simulates a blocking operation
print(#function, "finished", Thread.isMainThread, Thread.current)
return UIImage(systemName: "sun.max")!
}
}
A synchronous function blocking the current thread for 10 seconds.

The very first step is adding an async version, which is a matter of wrapping it with a Task.

struct ImageProcessor {
func process(_ imageData: Data) async -> UIImage {
print(#function, Thread.isMainThread, Thread.current)
let image = await Task {
syncProcessData(imageData)
}.value
print(#function, Thread.isMainThread, Thread.current)
return image
}
}
An async process function wrapping a synchronous long-running function.

What happens inside the function is that we created a new Task object which defines the work we want to run. Since we want to return an instance of UIImage as part of this function, we need to wait until the created task finishes. Therefore, we access the return value of the task using the value property which is an async property. Since it is async, we need to use the await keyword which tells the runtime that the function flow could be suspended here. As the process function is async then this function can only be called from an async context. For example, this function is called from another task. Then the task is added as a child task to the calling, parent, task. OK, so let’s use this code in a view model used by a SwiftUI view.

@MainActor
final class ViewModel: ObservableObject {
let imageProcessor = ImageProcessor()
@Published var currentImage: UIImage?
func applyEffectsToImage() {
print(#function, Thread.isMainThread, Thread.current)
Task {
print(#function, Thread.isMainThread, Thread.current)
let imageData = Data()
currentImage = await imageProcessor.process(imageData)
}
}
}
view raw ViewModel.swift hosted with ❤ by GitHub
A view model which triggers the asynchronous work.

Above is a view model which is used by a SwiftUI view. The applyEffectsToImage() function is called by a button and the published image is displayed by the view. This view model is a main actor which means that all the properties and functions will run on the main thread. Since we want to call an async process function, we need to create an async context. This is where the Task comes into the play again. If we do not have an async context and create a task then that task will run on the actor it was created on. In this case, it runs on the main thread. But if the task creates a child task then that task will run on a background thread. In the example above, the task’s closure runs on the main thread until the closure is suspended when calling an async process function.

If we take a look at which threads are used, then it looks like this:

applyEffectsToImage() true <_NSMainThread: 0x600000908300>{number = 1, name = main}
applyEffectsToImage() true <_NSMainThread: 0x600000908300>{number = 1, name = main}
process(_:) false <NSThread: 0x600000901040>{number = 4, name = (null)}
syncProcessData(_:) started false <NSThread: 0x600000979100>{number = 7, name = (null)}
syncProcessData(_:) finished false <NSThread: 0x600000979100>{number = 7, name = (null)}
process(_:) false <NSThread: 0x600000979100>{number = 7, name = (null)}

Here we can see that the task in the view model runs on the main actor and therefore on the main thread. Methods in the image processor run on the background threads instead – exactly what we wanted. The long-running function does not block the main thread.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift

A few examples of async await in Swift

Async await was added to Swift 5.5 and brought a new way how to write asynchronous code. Functions can be annotated with an async keyword, which enables them to be called with the await keyword. When an async function is being awaited, the execution of the current context is suspended and is resumed when the async function finishes. There are a lot more details in the SE-0296. In this blog post, we’ll take a look at a few examples of async await.

Wrapping completion handler based function

We can add a separate async functions for completion hander code. Which is nice since we do not reimplement everything we have using completion handlers. Take a look at a bit longer explanation in Adding async await to existing completion handler based code.

public func storeData(_ dataProvider: @escaping () -> Data?, identifier: Identifier = UUID().uuidString) async throws -> Identifier {
return try await withCheckedThrowingContinuation({ continuation in
self.storeData(dataProvider, identifier: identifier) { result in
continuation.resume(with: result)
}
})
}
Wrapping a completion handler function which uses the Result type.

Calling async function from non-async context

When using the await keyword then the current asynchronous context suspends until the awaited async function finishes. Suspension can only happen if we are in an asynchronous context. Therefore, a function without the async keyword can’t use await directly. Fortunately, we can create asynchronous contexts easily with a Task. One example of this is when we use MVVM in SwiftUI views and the view model has different methods we want to call when, for example, a user taps on a button

func refreshDocuments() {
Task(priority: .userInitiated) {
do {
self.documents = try await fetcher.fetchAllDocuments()
}
catch {
// Handle error
}
}
}
Creating a Task for enabling to use await on an async function.

Running tasks concurrently

Sometimes we have several independent tasks we need to complete. Let’s take an example when we want to preload messages for conversations. The code snippet below takes an array of conversations and then starts loading messages for each of the conversation. Since conversations are not related to each other, we can do this concurrently. This is what a TaskGroup enables us to do. We create a group and add tasks to it. Tasks in the group can run at the same time, which can be a time-saver.

func preloadMessages(for conversations: [Conversation]) async {
await withThrowingTaskGroup(of: Void.self) { taskGroup in
for conversation in conversations {
taskGroup.addTask {
try await self.preloadMessages(for: conversation)
}
}
}
}

Retrying a task with an exponential delay

This is especially related to networking code, where we might want to retry a couple of times before giving up and displaying an error. Additionally, we might want to wait before each request, and probably we want to wait a bit longer with each delay. Task has a static function detached(priority:operation:) so let’s create a similar retried() static function. In addition to priority and operation, we have arguments for defining how many times to retry and how much to delay, where the delay is increased exponentially with each retry. The first retry attempt is delayed by default 1 second, then the next 2 seconds, the third 4 seconds and so on. If the task happens to be cancelled while waiting, then the Task.sleep(nanoseconds:) throws CancellationError.

extension Task {
static func retried(times: Int, backoff: TimeInterval = 1.0, priority: TaskPriority? = nil, operation: @escaping @Sendable () async throws -> Success) -> Task where Failure == Error {
Task(priority: priority) {
for attempt in 0..<times {
do {
return try await operation()
}
catch {
let exponentialDelay = UInt64(backoff * pow(2.0, Double(attempt)) * 1_000_000_000)
try await Task<Never, Never>.sleep(nanoseconds: exponentialDelay)
continue
}
}
return try await operation()
}
}
}
Retrying a Task with an exponential delay.

self.documents = try await Task.retried(times: 3) {
try await self.fetcher.fetchAllDocuments() // returns [Documents]
}.value
Example of Task.retried() with accessing the returned value.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
Swift

Adding async await to existing completion handler based code

Xcode 13 with Swift 5.5 toolchain brings async-await to the Swift language. The aim of this blog post is not to cover everything it brings, but instead concentrate on seeing how completion handler based code can be wrapped with async await. In the end, we have reused existing code but made it available to be called from async task contexts. For demonstrating this, we’ll take my IndexedDataStore Swift package and add async await supported methods to existing completion handler based API.

The IndexedDataStore package has async methods for loading data and storing data.

func loadData<T>(forIdentifier identifier: Identifier, dataTransformer: @escaping (Data) -> T?, completionHandler: @escaping (T?) -> Void)

func storeData(_ dataProvider: @escaping () -> Data?, identifier: Identifier = UUID().uuidString, completionHandler: @escaping (Result<Identifier, Error>) -> Void) {

We can easily add async await methods to these completion hander based methods. We’ll need to use one of the withCheckedXXXContinuation functions. This allows us to add a method to IndexedDataStore class like this:

public func loadData<T>(forIdentifier identifier: Identifier, dataTransformer: @escaping (Data) -> T?) async -> T? {
return await withCheckedContinuation({ continuation in
self.loadData(forIdentifier: identifier, dataTransformer: dataTransformer) { object in
continuation.resume(returning: object)
}
})
}

What happens here is that we use the async withCheckedContinuation function, which gives us a hook into async await machinery. The function suspends the current task in hand and calls the completion handler with a continuation object. Then we can use our completion handler based async code and resume the suspended task when we are done by calling resume.

When we are dealing with completion handlers which use the Result type or provide an error value, then we’ll need to use withCheckedThrowingContinuation instead. This function provides a continuation object which supports throwing errors. This is exactly the case with the storeData function.

public func storeData(_ dataProvider: @escaping () -> Data?, identifier: Identifier = UUID().uuidString) async throws -> Identifier {
return try await withCheckedThrowingContinuation({ continuation in
self.storeData(dataProvider, identifier: identifier) { result in
continuation.resume(with: result)
}
})
}

When it comes to unit-testing then async await makes the testing code so much shorter. We can annotate test functions with the async keyword and do not need to deal with XCTestExpectations.

func testStoreAndLoadAsync() async throws {
let identifier = try await dataStore.storeData({ "Data".data(using: .utf8) }, identifier: "abc")
XCTAssertEqual(identifier, "abc")
let string = await dataStore.loadData(forIdentifier: "abc", dataTransformer: { String(decoding: $0, as: UTF8.self) })
XCTAssertEqual(string, "Data")
}

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Categories
iOS macOS Swift SwiftUI

Image converter with AsyncSequence

WWDC’21 brought us a new protocol named AsyncSequence. As the name stands, it represents a sequence of asynchronous elements. For trying out the new API we’ll build a tiny ThumbnailSequence which takes in a list of image names and by iterating the sequence, we’ll get back scaled thumbnails for those image names one by one. The image scaling runs on a background thread.

The AsyncSequence protocol comes with two associated types: Element and AsyncIterator. Element represents the type which is produced by the sequence, and AsyncIterator is the type responsible for reproducing elements. Same as with sequences, but the main difference is that accessing each of the element is asynchronous. Therefore, for creating a custom type ThumbnailSequence which conforms to AsyncSequence we’ll, set the associated type Element to be equal to UIImage, and implement a custom iterator. ThumbnailSequence initializers takes a list of image names and also defines a max scaled image size. Additionally, we’ll take advantage of the new byPreparingThumbnail(ofSize) async method for scaling the image. Implementation of the async sequence is shown below:

struct ThumbnailSequence: AsyncSequence {
typealias AsyncIterator = Iterator
typealias Element = UIImage
var imageNames: [String]
let maxSize: CGSize
func makeAsyncIterator() -> Iterator {
return Iterator(imageNames: imageNames, maxSize: maxSize)
}
struct Iterator: AsyncIteratorProtocol {
typealias Element = UIImage
var imageNames: [String]
let maxSize: CGSize
mutating func next() async -> UIImage? {
guard !imageNames.isEmpty else { return nil }
guard let image = UIImage(named: imageNames.removeFirst()) else { return nil }
let ratio = image.size.height / maxSize.height // simplified scaling
return await image.byPreparingThumbnail(ofSize: CGSize(width: image.size.width / ratio, height: maxSize.height))
}
}
}

With a simple async sequence created, we can hook it up to a SwiftUI view. WWDC’21 also brought a new task view modifier, which is invoked when the view appears and cancelled when the view is removed. In the task view modifier we’ll loop over the sequence and one by one load UIImages which then are set to a local images array which in turn is connected to a LazyVStack. The flow we’ll get is that we are loading images one by one, and after every image load we’ll add a new item to the stack.

struct ContentView: View {
let imageNamesToLoad = ["Screenshot1", "Screenshot2", "Screenshot3", "Screenshot4", "Screenshot5", "Screenshot6", "Screenshot7", "Screenshot8", "Screenshot9", "Screenshot10"]
@State private var images = [UIImage]()
var body: some View {
ScrollView {
LazyVStack {
ForEach(images, id: \.self) { image in
Image(uiImage: image)
}
}
}
.task {
for await image in ThumbnailSequence(imageNames: imageNamesToLoad, maxSize: CGSize(width: 256, height: 256)) {
print("Loaded \(image)")
self.images.append(image)
}
}
}
}
Screenshot of a iPhone simulator showing a view with vertical list of images.
The final sample app displaying images in a vertical stack.

Summary

In this post, we took a quick look at the AsyncSequence protocol and created a pipeline which converts image names to scaled image instances one by one. After that, we connected the pipeline to a SwiftUI view.

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.