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.