I’m having trouble making async functions run in background threads (to prevent blocking the main thread).
Below is a method that takes about 5 seconds to run.
From what I’ve learned, it seemed like making the function async
and marking it with await
on function call would be enough. But it doesn’t work as intended and still freezes up the UI.
EDIT
Since it’s stated that Swift 5.5 concurrency can replace DispatchQueue, I am trying to find a way to do this with only Async/Await.
EDIT_2
I did try removing the @MainActor wrapper, but it still seem to run on the main thread.
NumberManager.swift
@MainActor class NumberManager: ObservableObject {
@Published var numbers: [Double]?
func generateNumbers() async {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
self.numbers = numbers
// takes about 5 seconds to run...
} }
ContentView
struct ContentView: View {
@StateObject private var numberManager = NumberManager()
var body: some View{
TabView{
VStack{
DetailView(text: isNumbersValid ? "First number is: (numberManager.numbers![0])" : nil)
.onAppear() {
Task {
// Runs in the main thread, freezing up the UI until it completes.
await numberManager.generateNumbers()
}
}
}
.tabItem {
Label("One", systemImage: "list.dash")
}
Text("Hello")
.tabItem {
Label("Two", systemImage: "square.and.pencil")
}
}
}
var isNumbersValid: Bool{
numberManager.numbers != nil && numberManager.numbers?.count != 0
} }
What I’ve tried…
I’ve tried a few things, but the only way that made it run in the background was changing the function as below. But I know that using Task.detached should be avoided unless it’s absolutely necessary, and I didn’t think this is the correct use-case.
func generateNumbers() async {
Task.detached {
var numbers = [Double]()
numbers = (1...10_000_000).map { _ in Double.random(in: -10...10) }
await MainActor.run { [numbers] in
self.numbers = numbers
}
}
3
Answers
You have answered your own question – You can use structured concurrency to solve your problem.
Your problem occurs because you have use the
@MainActor
decorator on your class. This means that it executes on the main queue.You can either remove this decorator or, as you have found, use structured concurrency to explicitly create a detached task and the use a main queue task to provide your result.
Which approach you use depends on what else this class does. If it does a lot of other work that needs to be on the main queue then
@MainActor
is probably a good approach. If not then remove it.Writing
async
on a function doesn’t make it leave the thread. You need a continuation and you need to actually leave the thread somehow.Some ways you can leave the thread using
DispatchQueue.global(qos: .background).async {
or useTask.detached
.But the most important part is returning to the
main
thread or even more specific to the Actor’s thread.DispatchQueue.main.async
is the "old" way of returning to the main thread it shouldn’t be used withasync await
. Apple as providedCheckedContinuation
andUncheckedContinuation
for this purpose.Meet async/await can elaborate some more.
The
@MainActor
property wrapper is not (just) what makes your observable object run on the main thread.@StateObject
is what’s doing it. Which is logical, since changes to the object will update the UI.Removing the
@MainActor
wrapper is not the solution, because any changes to@Published
properties will have to be done on the main thread (since they update the UI). You also don’t want to runTask.detached
, at least not if the task is going to change any@Published
property, for the same reason.By marking your
generateNumbers
function asasync
, a method from the main thread can call it withawait
– which allows the task to suspend and not block the main thread. That’s what makes it concurrent.A more complete
loadNumbers
method could also store a reference to the task allowing you to cancel or restart a running task. However nowadays we have the excellent.task(priority:_:)
to do all of that for you. It manages the task’s lifecycle automatically, which means less boilerplate code.As far as I know this is the most succinct way to do expensive calculations on a background thread in Swift 5.7.