You asked:
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
?
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 old DispatchQueue
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.
When I tap “Start Task” button, button is pressed until the countupAsync()
is done and not update “Count” on screen.
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:
Recall that with Swift, the language allows us to uphold a runtime contract that threads will always be able to make forward progress. It is based on this contract that we have built a cooperative thread pool to be the default executor for Swift. As you adopt Swift concurrency, it is important to ensure that you continue to maintain this contract in your code as well so that the cooperative thread pool can function optimally.
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:
func countUpAsync() async throws {
print("countUpAsync() isMain=\(Thread.isMainThread)")
for _ in 0..<5 {
count += 1
try await Task.sleep(nanoseconds: NSEC_PER_SEC / 2)
}
}
and
Button("Start Task") {
Task {
try await viewModel.countUpAsync()
}
}
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:
I want to run a long process and display the progress.
Above I told you how to avoid blocking with Thread.sleep
API (by using the non-blocking Task
rendition). But I suspect that you used sleep
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.)
func countSequence() async {
let stream = AsyncStream(Int.self) { continuation in
Task.detached {
for _ in 0 ..< 5 {
// do some slow and synchronous calculation here
continuation.yield(1)
}
continuation.finish()
}
}
for await value in stream {
count += value
}
}
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.