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.

One reply on “Wrapping a long-running function with async-await in Swift”