Update20
Fighting with sendable
Now I’ve enabled strict concurrency checking in preparation for Swift 6, like everyone else, I’ve been fighting with Sendability warnings, especially when trying to return data from a task.
Update 2:
Matt Massicote linked me to his Concurrency Recipes which has a way to do what I want without needing the closure to be Sendable
which works much nicer. Create an async wrapper around the synchronous code that generates the results, putting it inside a CheckedContinuation
and inside that continuation it runs the generator on the DispatchQueue
nonisolated
func getResults() async -> [String] {
await withCheckedContinuation { continuation in
DispatchQueue.global().async { [weak self] in
let result = []
continuation.resume(returning: result)
}
}
}
Now it can just be called with
@MainActor
func doSomething(resultsClosure: @escaping ([String]) -> Void) {
Task { [weak self] in
if let results = await self?.getResults() {
try resultsClosure(results)
}
}
}
because the resultsClosure
is only called inside the MainActor
isolation.
Original post with the more complicated way
func doSomething(resultsClosure: @escaping ([String]) -> Void) {
Task.detached {
let results: [String] = []
resultsClosure(results)
}
}
Capture of 'resultsClosure' with non-sendable type '([String]) -> Void' in a `@Sendable` closure
The problem, I think, is that the closure isn’t sendable because [String]
isn’t sendable because it could, in theory (though not in practice here), be mutated by the closure.
So I created a custom struct wrapping the array as a nonmutable property
struct SendableResults: Sendable {
public let results: [String]
}
func doSomething(resultsClosure: @escaping (SendableResults) -> Void) {
Task.detached {
let results = SendableResults(results: [])
resultsClosure(results)
}
}
And the closure needs to be explicitly required to be Sendable
func doSomething(resultsClosure: @Sendable @escaping (SendableResults) -> Void)
Now this makes the closure required to be isolated to the detached Task, and I’m calling a MainActor isolated function inside it so I get an error.
Call to main actor-isolated instance method 'setResults' in a synchronous nonisolated context
so I need to add an await
to the setResults
call
resultsGenerator.doSomething { [weak self] in
let results = $0.results
await self?.setResults(results)
}
but that’s made the closure async, so need to declare it as such and await it when calling the closure.
func doSomething(resultsClosure: @Sendable @escaping (SendableResults) async -> Void) {
Task.detached {
let results = SendableResults(results: [])
await resultsClosure(results)
}
}
I feel like there’s probably some way I could mark it all as unchecked sendables to shut the compiler up, but in my mind that’s the same as force unwrapping a variable because “I know it’ll never be nil” - it’s a hack that will come back to bite me later.
Update 1:
As Matt Massicote pointed out on Mastodon, I don’t need to wrap [String]
inside SendableResults
, the warning was just about the closure needing @Sendable
annotation.