Suppose you have a method that executes asynchronously in a global context. Depending on the execution you need to update the UI.
private func fetchUser() async {
do {
let user = try await authService.fetchCurrentUser()
view.setUser(user)
} catch {
if let error = error {
view.showError(message: error.message)
}
}
}
Where is the correct place to switch to the main thread?
- Assign
@MainActor
to thefetchUser()
method:
@MainActor
private func fetchUser() async {
...
}
- Assign
@MainActor
to thesetUser(_ user: User)
andshowError(message: String)
view's methods:
class SomePresenter {
private func fetchUser() async {
do {
let user = try await authService.fetchCurrentUser()
await view.setUser(user)
} catch {
if let error = error {
await view.showError(message: error.message)
}
}
}
}
class SomeViewController: UIViewController {
@MainActor
func setUser(_ user: User) {
...
}
@MainActor
func showError(message: String) {
...
}
}
- Do not assign
@MainActor
. Useawait MainActor.run
orTask
with@MainActor
instead to runsetUser(_ user: User)
andshowError(message: String)
on the main thread (likeDispatchQueue.main.async
):
private func fetchUser() async {
do {
let user = try await authService.fetchCurrentUser()
await MainActor.run {
view.setUser(user)
}
} catch {
if let error = error {
await MainActor.run {
view.showError(message: error.message)
}
}
}
}
CodePudding user response:
Option 2 is logical, as you are letting functions that must run on the main queue, declare themselves as such. Then the compiler can warn you if you incorrectly call them. Even simpler, you can declare the class that has these functions to be @MainActor
, itself, and then you don't have to declare the individual functions as such. E.g., because a well-designed view or view controller limits itself to just view-related code, it is safe for that whole class to be declared as @MainActor
and be done with it.
Option 3 (in lieu of option 2) is brittle, requiring the app developer to have to remember to manually run
them on the main actor. You lose compile-time warnings should you fail to do the right thing. Compile-time warnings are always good. But WWDC 2021 video Swift concurrency: Update a sample app points out that even if you adopt option 2, you might still use MainActor.run
if you need to call a series of MainActor
methods and you might not want to incur the overhead of await
ing one call after another, but rather wrap the group of main actor functions in a single MainActor.run
block. (But you might still consider doing this in conjunction with option 2, not in lieu of it.)
In the abstract, option 1 is arguably a bit heavy-handed, designating a function that does not necessarily have to run on the main actor to do so. You should only use the main actor where it is explicitly needed/desired. That having been said, in practice, I have found that there is often utility in having presenters (or controllers or view models or whatever pattern you adopt) run on the main actor, too. This is especially true if you have, for example, synchronous UITableViewDataSource
or UICollectionViewDataSource
methods grabbing model data from the presenter. If you have the relevant presenter using a different actor, you cannot always return to the data source synchronously. So you might have your presenter methods running on the main actor, too. Again, this is best considered in conjunction with option 2, not in lieu of it.
So, in short, option 2 is prudent, but is often married with options 1 and 3 as appropriate. Routines that must run on the main actor should be designated as such, rather than placing that burden on the caller.
The aforementioned Swift concurrency: Update a sample app covers many of these practical considerations and is worth watching if you have not already.