Disclaimer: this code is simplified for demonstration purposes.
Suppose I have a value only available asynchronously:
struct Source: Sendable {
var someAsyncValue: Int {
get async {
42
}
}
}
That value is a part of a dependency:
class Dependency {
let source = Source()
var someAsyncValue: Int {
get async {
await source.someAsyncValue
}
}
}
And this is then used in some code:
struct Container {
let dependency = Dependency()
@MainActor func worksFine() async {
_ = await self.dependency.source.someAsyncValue // ✅
}
}
This is a repeated pattern, so I introduce a "shortcut" into Dependency
, so I don't have to write .source
every time:
extension Dependency {
var someAsyncValue: Int {
get async {
await source.someAsyncValue
}
}
}
Predictably, this doesn't work, because dependency
is neither Sendable
, nor @MainActor
.
struct Container {
let dependency = Dependency()
@MainActor func doesNotWork() async {
_ = await self.dependency.someAsyncValue // ❌ Non-sendable type 'Dependency' exiting main actor-isolated context in call to nonisolated property 'someAsyncValue' cannot cross actor boundary
}
}
This Dependency
could easily be made Sendable
, but the real one has mutable state, so that's out. It could be made into an actor
or isolated to @MainActor
, but the real version also conforms to protocols, which is another can of worms.
The method on Container
is @MainActor
because some other non-Sendable
classes are later involved, calling more async
methods. Removing @MainActor
would just shift the problem elsewhere.
I could mark someAsyncValue
with @MainActor
, but it's a simple value... there is really no reason it should have to be isolated.
I only want to be able to avoid having to write .source
, not rearchitect the whole app . My question is whether I've overlooked something (please) or whether I'll just have to bite the bullet here.