I'm facing an issue while trying to modify a static
property within a subclass of UIViewController
from another class, specifically inside an asynchronous function. Additionally, I'm encountering the error
Main actor-isolated static property 'text' cannot be mutated from a non-isolated context; this is an error in Swift 6
My code:
class Primary: UIViewController {
static var text = ""
}
class Secondary {
func someAction() async {
Primary.text = "12" // Getting warning in this line!
}
}
Anyhow by using DispatchQueue.main.async
will silence this warning.
My question is that why static property in UIViewController
is considered as an actor isolated property? If it is isolated, then why can't we use await
(just curious) to access that property?
You said:
My question is that why static property in
UIViewController
is considered as an actor isolated property?
If you look at the UIViewController
documentation, it tells us that the whole class is isolated to the main actor, defined as follows:
@MainActor
class UIViewController : UIResponder
Because UIViewController
is isolated to the main actor, all subclasses (and any subclasses’ properties, methods, etc.) are also isolated to the main actor.
If it is isolated, then why can’t we use
await
(just curious) to access that property?
The error message is a little misleading. Yes, part of the problem is that you are trying to perform some action in a different asynchronous context (which begs for await
). But the deeper problem is that you are trying to reach inside and directly update an actor-isolated property, at all. The actor is responsible for handling any mutation of its properties. External types should not be able to mutate its properties directly.
The whole idea of using actors is that the actor should facilitate all mutation of its own properties. This is done not only to prevent data races, but also to eliminate race conditions, more generally. See WWDC 2021’s video Protect mutable state with Swift actors.
But you can provide a method to mutate this property:
class Primary: UIViewController {
static var text = ""
static func updateText(_ string: String) {
text = string
}
}
class Secondary {
func someAction() async {
Primary.updateText("12") // Error: Expression is 'async' but is not marked with 'await'
}
}
Now that we are updating that property correctly, you get the appropriate error about needing to await
this update. Thus, the corrected implementation would be:
class Secondary {
func someAction() async {
await Primary.updateText("12") // OK
}
}
As discussed below, I think the whole idea of mutable static
properties is suspect, so I would not advise the above. But I am just trying to illustrate how, in general, one might theoretically resolve the compiler error you described.
A few unrelated observations:
It is very curious to have static
mutable properties. If the static
was introduced merely to provide a convenient way of allowing Secondary
to mutate some property of Primary
, I would strongly advise against that pattern.
A static
should be used only where you really need all instances of Primary
have some shared state. That is the only purpose of static
. The problem with the mutable static
properties is that dependencies between types are hidden, leading to code that is exceedingly hard to debug. It is akin to the vibrant debate about why globals are “evil”. (Lol.) Dependencies are hidden and the mutating state is hard to control/debug.
In short, mutable statics are exceedingly unusual for a view controllers, and is generally code-smell.
The idea of one type reaching in and mutating a property of another type is antithetical to the goal of keeping types loosely coupled. E.g., Secondary
should provide general-purpose mechanisms to allow its properties to be observed, but make no assumptions about Primary
. Primary
should merely instantiate Secondary
and observe its properties.
E.g., with ObservableObject
, you might use Combine to observe changes:
import UIKit
import Combine
class Primary: UIViewController {
private let secondary = Secondary()
@IBOutlet weak var label: UILabel!
private var cancellables: Set<AnyCancellable> = []
override func viewDidLoad() {
super.viewDidLoad()
secondary.$value
.sink { [weak self] value in
self?.label.text = "\(value)"
}
.store(in: &cancellables)
}
@IBAction func didTapButton(_ sender: Any) {
Task { await secondary.someAction() }
}
}
@MainActor
class Secondary: ObservableObject, Sendable {
@Published var value = 0
func someAction() async {
value += 1
}
}
This way, Secondary
is no longer tightly coupled with Primary
. In the above, Primary
merely observes changes in Secondary
.
There are many different patterns (observation, closures, delegates) that achieve the same goal. But ideally, we want our types loosely coupled. Primary
should avail itself of Secondary
’s interface, but Secondary
should not make any assumptions about Primary
.