Sekar
Sekar

Reputation: 31

Can I set a published property inside a async function in an ObservableObject class marked as @MainActor?

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

Answers (2)

Rob
Rob

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:

Upvotes: 2

malhal
malhal

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

Related Questions