The error is warning you that your class has a mutable property. That mutable property can be accessed from outside of Swift concurrency and is therefore not safe.
Consider the following:
final class Foo: Sendable {
@MainActor var counter = 0 // Stored property 'counter' of 'Sendable'-conforming class 'Foo' is mutable
}
We can now consider the following property and method of a view controller, which interacts with counter
directly:
let foo = Foo()
func incrementFooManyTimes() {
DispatchQueue.global().async { [self] in
DispatchQueue.concurrentPerform(iterations: 10_000_000) { _ in
foo.counter += 1
}
print(foo.counter) // 6146264 !!!
}
}
NB: If you do have set the “Swift Concurrency Checking” build setting to “Minimal” or “Targeted”, the above will compile with only the aforementioned warning. (If you change this to “Complete”, it becomes a hard error.)
In short, you have marked the as @MainActor
, but there is nothing to stop other threads from interacting with this property of the class directly. For a type to be Sendable
, it must either:
- be immutable;
- manually synchronize its properties; or
- be an
actor
If you are going to have a non-actor be Sendable
with mutable properties, you have to implement the thread-safety yourself. E.g.:
final class Foo: @unchecked Sendable {
private var _counter = 0
private let queue: DispatchQueue = .main // I would use `DispatchQueue(label: "Foo.sync")`, but just illustrating the idea
var counter: Int { queue.sync { _counter } }
func increment() {
queue.sync { _counter += 1 }
}
}
And
func incrementFooManyTimes() {
DispatchQueue.global().async { [self] in
DispatchQueue.concurrentPerform(iterations: 10_000_000) { _ in
foo.increment()
}
print(foo.counter) // 10000000
}
}
Obviously, you could also restrict yourself to immutable properties and no synchronization would be necessary. But I assume you needed mutability.
Now, in this mutable scenario, you can use whatever synchronization mechanism you want, but hopefully this illustrates the idea. In short, if you are going to allow it to mutate outside of Swift concurrency, you have to implement the synchronization yourself. And because we are implementing our own synchronization, we tell the compiler that it is @unchecked
, meaning that you are not going to have the compiler check it for correctness, but rather that burden falls on your shoulders.
Obviously, life is much easier if you use an actor and stay within the world of Swift concurrency. E.g.:
actor Bar {
var counter = 0
func increment() {
counter += 1
}
}
And:
let bar = Bar()
func incrementBarManyTimes() {
Task.detached {
await withTaskGroup(of: Void.self) { group in
for _ in 0 ..< 10_000_000 {
group.addTask { await self.bar.increment() }
}
await print(self.bar.counter)
}
}
}