How I can return to main thread when using async/await concurrency mechanism in new Swift 5.5? Should I just mark function, class with @MainActor.
Can I still use DispatchQueue.main.async? Will it be correct? As new mechanism doesn't use GCD and there is no mapping between async tasks and thread like before?
For example I am using SwiftUI List with refreshable
List { }
.refreshable {
viewModel.fetchData()
}
Is this ok
List { }
.refreshable {
DispatchQueue.main.async {
viewModel.fetchData()
}
}
Or I need to add @MainActor on ViewModel class? I doesn't use async/await in project so using MainActor just for this single refreshable seems redundant, I also doesn't know how adding such attribute influance remaining methods and properties of ViewModel class, they now use Combine.
But on the other hand Xcode displays
runtime: SwiftUI: 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.
Moreover after adding @MainActor to ViewModel I am getting multiple warnings like this
Property 'title' isolated to global actor 'MainActor' can not satisfy corresponding requirement from protocol 'OnlineBankingListViewModelProtocol'
You asked:
Can I still use
DispatchQueue.main.async?
If you are in an async method and want to dispatch something to the main queue, the most literal equivalent would be:
MainActor.run { ... }
But it is more prudent to simply mark the method (or its class) with @MainActor. Not only will this ensure that it runs it on the main thread, but you get compile-time warnings if you attempt to call it from the wrong actor.
So, if your view model is marked with @MainActor, the manual running of the task on the MainActor becomes unnecessary. This is especially true when dealing with published properties of an observed object.
For example, consider:
@MainActor
class ViewModel: ObservableObject {
@Published var values: [Int] = []
func fetchData() async {
let foo = await ...
values = foo.values
}
}
And then
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
List {
...
}
.refreshable {
await viewModel.fetchData()
}
}
}
(Note, I made fetchData an async method and await it within refreshable so that the spinner accurately reflects when the async process is running.)
See WWDC 2021 video Swift concurrency: Update a sample app. That is admittedly illustrating the transition of a UIKit app, but includes examples of @MainActor and MainActor.run.
Note, while @MainActor, largely eliminates the need for MainActor.run { … }, there are still some scenarios where you might use this run pattern. Specifically, if you are on some other actor and want to run, for example, three separate @MainActor functions in succession on the main thread, you can wrap the series of them within a single MainActor.run { … } block, thereby running all three with a single dispatch to the main actor, rather than three separate calls.
Above, I focused on the salient portions, but here is my full MCVE:
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
List {
ForEach(viewModel.values, id: \.self) { value in
Text("\(value)")
}
}
.refreshable {
await viewModel.fetchData()
}
}
}
struct Foo: Decodable{
let json: [Int]
}
@MainActor
class ViewModel: ObservableObject {
@Published var values: [Int] = []
func fetchData() async {
do {
let foo = try await object(Foo.self, for: request)
values = foo.json
} catch {
print(error)
}
}
func object<T: Decodable>(_ type: T.Type, for request: URLRequest) async throws -> T {
let (data, response) = try await URLSession.shared.data(for: request)
guard let response = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
guard 200 ... 299 ~= response.statusCode else {
throw ApiError.failure(response.statusCode, data)
}
return try JSONDecoder().decode(T.self, from: data)
}
var request: URLRequest = {
let url = URL(string: "https://httpbin.org/anything")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.httpBody = "[1,2,3,4,5]".data(using: .utf8)
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue("application/json", forHTTPHeaderField: "Accept")
return request
}()
}
enum ApiError: Error {
case failure(Int, Data)
}
The replacement for DispatchQueue.main.async { foo.bar() } is:
Task { @MainActor in
print(Thread.current.isMainThread) // "true"
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With