Say I have this code:
class Presenter {
var viewToUpdate: UIView!
func updateUI() {
viewToUpdate.backgroundColor = .red
}
}
class ShinyNewAsyncAwaitClass {
func doAsyncAwaitThing() async {
// make network call or something
}
}
class OtherClassThatICantUpdateToAsyncAwaitYet {
func doOldClosureBasedThing(completion: @escaping () -> Void) {
// make network call or something
completion()
}
}
class TheClassThatUsesAllThisStuff {
var newClass: ShinyNewAsyncAwaitClass!
var oldClass: OtherClassThatICantUpdateToAsyncAwaitYet!
var presenter: Presenter!
func doSomethingWithNewClass() {
Task {
await self.newClass.doAsyncAwaitThing()
// ---->>> What do I do here? <<<<----
await self.presenter.updateUI()
}
}
func doSomethingWithOldClass() {
oldClass.doOldClosureBasedThing {
DispatchQueue.main.async {
self.presenter.updateUI()
}
}
}
func justUpdateTheView() {
self.presenter.updateUI()
}
}
In short, I have three classes. One I can update to async/await, the other I can’t, and one that uses both. Both need access to a function that updates UI, both will need to access that function on the main thread.
I saw somewhere I can add @MainActor
to the updateUI
function, but in cases where I’m already on the main thread and I just want to call updateUI
, like in justUpdateTheView
I get this error:
Call to main actor-isolated instance method ‘updateUI()’ in a synchronous nonisolated context
Add ‘@MainActor’ to make instance method ‘justUpdateTheView()’ part of global actor ‘MainActor’
I can’t define justUpdateTheView
as @MainActor
, because we’re trying to update our project to the new concurrency stuff slowly and this would cause a chain reaction of changes that need to be made.
What to do for the best? Can I do something like this:
func doSomethingWithNewClass() {
Task {
await self.newClass.doAsyncAwaitThing()
DispatchQueue.main.async {
self.presenter.updateUI()
}
}
}
It compiles, but are there any gotchas to be aware of?
2
Answers
You can do something like this to run the UI code on the
MainActor
:In GCD, we used to encumber the caller with the burden of making sure it dispatched the call to the right queue.
In Swift concurrency, we isolate the called function to the correct actor, and the compiler will automatically warn us if we ever attempt to call it directly from a function not isolated to the same actor. E.g., as you noted:
Yes, that is prudent. So you might do:
Or, better, in the case of a “presenter” (whose whole purpose is to interact with the view), isolate the whole type to the main actor:
You then said:
So, perhaps needless to say, the above actor-isolated
updateUI
that solves yourdoSomethingWithNewClass
question, reducing it simply to:No manual
DispatchQueue.main.async {…}
orMainActor.run {…}
is needed.And then, re
justUpdateTheView
, you can do:Or, as the compiler suggested:
(Personally, I would lean towards the former rather than sprinkling various service classes with
@MainActor
, but both work.)Anyway, once you have isolated
updateUI
to the main actor, theMainActor.run {…}
is no longer necessary. IMHO, when I find myself usingMainActor.run {…}
, I treat it as code-smell. (The exception to this rule is where you want to call a whole series of separate functions isolated to the main actor, you might wrap the group of them within aMainActor.run {…}
to have one context switch as opposed to a series of them.)Two asides:
Until such point that you have an opportunity to refactor that legacy API, I would go ahead and give it an
async
wrapper for the time being. Xcode actually provides a “Refactor” tool to do this for you:Then you end up with:
Or, if this is in a codebase that you can’t edit, move this wrapper into an extension:
Sure, you undoubtedly want to properly refactor the old API to use Swift concurrency, but the above provides a temporary wrapper that you can use until you get around to that. It only takes a few seconds and it saves you from encumbering your new code with legacy patterns.
I would advise caution in using
Task {…}
as that is unstructured concurrency (meaning that you lose task cancelation). TheTask {…}
is useful if you have to bridge from a synchronous context to an asynchronous one, but otherwise, use it with discretion.E.g., using our above wrapper and
updateUI
that is isolated to the main actor, it reducesTheClassThatUsesAllThisStuff
to something that stays within structured concurrency:For what it is worth, in WWDC 2021 video Swift concurrency: Update a sample app, they walk us through the transition from
DispatchQueue.main.async {…}
toMainActor.run {…}
to@MainActor
-isolation. They even walk through the aforementioned edge-case where one might still useMainActor.run {…}
. By the way, while I tried to drop you into the relevant portion of that video, the whole video is a nice, practical example of refactoring a legacy codebase.