Reputation: 31
Say I have a code as below:
@MainActor
class ViewModel: ObservableObject {
@Published var property1: Bool = false
func changeProperty() async {
property1 = false //Can I do this without DispatchQueue.main.async or MainActor.run?
}
}
func anotherFunction() {
Task {
await changeProperty()
}
}
I am learning SwiftUI and trying to get an understanding of how async/await works with the MainActor.
In the above code, my understanding is that since changeProperty
is an async function, when it is called from anotherFunction
with await, it will run on another thread and not the main.
With that assumption, can I change a published property from inside the async function which is not running on the main thread?
Does the @MainActor
declaration somehow automagically make that property1 = false
run on the main thread without the need for anything like the Dispatch.main.async?
Thank you!
Upvotes: 1
Views: 2312
Reputation: 438112
Having isolated ViewModel
to the main actor, you do not need to manually dispatch the updating of property1
to the main actor or queue. As SE-0316 Using global actors on a type says (emphasis added):
It is common for entire types (and even class hierarchies) to predominantly require execution on the main thread, and for asynchronous work to be a special case. In such cases, the type itself can be annotated with a global actor, and all of the methods, properties, and subscripts will implicitly be isolated to that global actor.
So, the following is isolates both property1
and the changeProperty
method to the main actor:
@MainActor
class ViewModel: ObservableObject {
@Published var property1 = false
func changeProperty() async {
property1 = true // no `DispatchQueue.main.async` or `MainActor.run` needed
}
}
Because they are both property1
and changeProperty
are isolated to the main actor, no further synchronization with the main thread is necessary. Note, it does not matter whether changeProperty
is a synchronous or asynchronous method: It can still safely update the actor-isolated properties. (Probably needless to say, you would never make changeProperty
an async
method if it did not have an await
somewhere within the method.)
As an aside, once you have adopted Swift concurrency, you should probably refrain from dispatching to GCD queues at all. Stick with the Swift concurrency API.
References:
If you are curious about the Swift concurrency threading model, WWDC 2021 video Swift concurrency: Behind the scenes may be interesting.
For more information on actors, actor-isolation, and the MainActor
global actor, see Protect mutable state with Swift actors.
In Swift concurrency: Update a sample app, they talk about the convention regarding isolating ObservableObject
classes (such as a view model) to the main actor.
Upvotes: 2
Reputation: 30746
Your object for an async view-related action would not normally be main actor because if you want to have your async func run on the main thread you might as-well declare it in the View
struct which already is main actor.
We don't need objects with complicated cancellation logic to do async work since .task
was introduced, but if you really want one for some reason then I've seen Apple using this pattern (StoreActor.swift
in the FoodTruck sample):
class Fetcher: ObservableObject {
@Published var property1: Bool = false
// background thread because this class is not @MainActor
func changeProperty() async {
let value = await Helper.anotherFunction()
Task { @MainActor in
property1 = value
}
}
}
If you call await fetcher.changeProperty
from a .task
all of it inc. that child @MainActor
Task
will be cancelled when the underlying UIView
disappears, very neat.
Again though, it would be a lot simpler to forget the object and just do:
@State var property = false
.task {
property = await Anything.someAsyncFunc()
}
Upvotes: -1