I’m studying async
, await
, @MainActor
of Swift.
I want to run a long process and display the progress.
import SwiftUI
@MainActor
final class ViewModel: ObservableObject {
@Published var count = 0
func countUpAsync() async {
print("countUpAsync() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
count += 1
Thread.sleep(forTimeInterval: 0.5)
}
}
func countUp() {
print("countUp() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
self.count += 1
Thread.sleep(forTimeInterval: 0.5)
}
}
}
struct ContentView: View {
@StateObject private var viewModel = ViewModel()
var body: some View {
VStack {
Text("Count=\(viewModel.count)")
.font(.title)
Button("Start Dispatch") {
DispatchQueue.global().async {
viewModel.countUp()
}
}
.padding()
Button("Start Task") {
Task {
await viewModel.countUpAsync()
}
}
.padding()
}
.padding()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
When I tap “Start Dispatch” button, the “Count” is updated but am warned:
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
I thought the class ViewModel
is @MainActor
, count
property is manipulated in Main
thread, but not.
Should I use DispatchQueue.main.async{}
to update count
although @MainActor
?
When I tap “Start Task” button, button is pressed until the countupAsync()
is done and not update Count on screen.
What is the best solution?
You asked:
One should avoid using
DispatchQueue
at all. Just use the new concurrency system where possible. See WWDC 2021 video Swift concurrency: Update a sample app for guidance about transitioning from the oldDispatchQueue
code to the new concurrency system.If you have legacy code with
DispatchQueue.global
, you are outside the new cooperative pool executor, and you cannot rely on an actor to resolve this. You would either have to manually dispatch the update back to the main queue, or, better, use the new concurrency system and retire GCD entirely.Yes, because it is running on the main actor and you are blocking the main thread with
Thread.sleep(forTimeInterval:)
. This violates a key precept/presumption of the new concurrency system that forward progress should always be possible. See Swift concurrency: Behind the scenes, which says:Now that discussion is in the context of unsafe primitives, but it applies equally to avoiding blocking API (such as
Thread.sleep(fortimeInterval:)
).So, instead, use
Task.sleep(nanoseconds:)
, which, as the docs point out, “doesn’t block the underlying thread.” Thus:and
The
async
-await
implementation avoids blocking the UI.In both cases, one should simply avoid old GCD and
Thread
API, which can violate assumptions that the new concurrency system might be making. Stick with the new concurrent API and be careful when trying to integrate with old, blocking API.You said:
Above I told you how to avoid blocking with
Thread.sleep
API (by using the non-blockingTask
rendition). But I suspect that you usedsleep
as a proxy for your “long process”.Needless to say, you will obviously want to make your “long process” run asynchronously within the new concurrency system, too. The details of that implementation are going to be highly dependent upon precisely what this “long process” is doing. Is it cancelable? Does it call some other asynchronous API? Etc.
I would suggest that you take a stab at it, and if you cannot figure out how to make it asynchronous within the new concurrency system, post a separate question on that topic, with a MCVE.
But, one might infer from your example that you have some slow, synchronous calculation for which you periodically want to update your UI during the calculation. That seems like a candidate for an
AsyncSequence
. (See WWDC 2021 Meet AsyncSequence.)Above I am using a detached task (because I have a slow, synchronous calculation), but use the
AsyncSequence
to get a stream of values asynchronously.There are lots of different approaches (which are be highly dependent upon what your “long process” is), but hopefully this illustrates one possible pattern.