So I am trying to migrate to Swift 6. I have read a ton of articles, but I still struggle with how to do this in practice. Especially when talking about sendable closures.
So let's say I have this code in my network layer that basically performs a DELETE call, and calls a completion handler with an Error?
func delete(withCompletion completion: @escaping (Error?) -> Void) {
var req = self.oAuth2.request(forURL: self.resource.url)
req.configure(withAcceptHeader: nil, andMethod: "DELETE")
if let requestModel = resource.requestModel {
req.httpBody = self.encode(model: requestModel)
}
let task = self.oAuth2.session.dataTask(with: req) { data, response, error in
if let error = error {
self.logger.e(error)
completion(error)
return
} else {
completion(nil)
}
}
task.resume()
}
This code gives the warning "Capture of 'completion' with non-sendable type '((any Error)?) -> Void' in a @Sendable closure; this is an error in the Swift 6 language mode". That's fine and I understand why. But what would be the "best" way to handle this, given I in this version want to retain the completion handler (otherwise it's a huge refactoring).
Is the correct way to handle this to make the completion handler @Sendable? Since dataTask's completion handler is sendable, I assume that is the correct way. But I feel like I'm just kicking the can down the street then, because then the hundreds of methods in the service layer that uses this (and the corresponding get and post methods) will just end up giving this warning. So I feel like I'm missing something.
@Sendable annotation on the closure is part of its signature, much like throws is part of a function/closure’s signature. When you change an function frobnicate from non throwing to be throwing, and frobnicate is used in 100 places, all those hundreds places need updating to themselves be changes to be using try frobnicate and calling scope to be themselves marked with throws (or surrounded with do catch). It cascades!
And this is much like that, but in another “dimension” if you will, the sendability dimension.
If the method you have lived inside a different target you can import said target with @preconcurrency I think. But it is just deferring the work you probably wanna do anyway.
With this signature, the completion handler is allowed to move across isolation boundaries. This is critical for callers to know! It is potentially unsafe, for example, to update the UI from this callback. But this depends on how self.oAuth2.session is configured. Perhaps this is always invoked on the main thread. Supporting this will almost certainly require a MainActor.assumeIsolated though.
But if that is the case, then this signature would make more sense.
The compiler is kinda forcing you to take some information that usually only exists in documentation and put it into the type system. Thinking that way always helps me.
To elaborate on the "kicking the can" aspect further, think of it this way: Assuming you use a more or less default session (that does not invoke the completion closure on the main queue): It has always been a requirement that the closure could be sent over to another isolation context. The compiler just did not have the means to check this at compile time.
Now that it has a means to syntactically express this, it does: That's the added @Sendable annotation to the closure. Since you wrapped it up in the same manner it is correct to repeat that in your function signature.
I assume when you say
You mean not further wrappers, but callers actually using the function you define. I.e. you have something like
myWrapper.delete() { error in
// here be some warning
}
So you don't specify the closure's type directly, it is actually inferred from delete's signature. A warning then indicates that the implementation of the closure does not match that type, and as far as I can see the only potential reason would be that you capture something in the closure that is either not Sendable or sending.
If that is the case, you always had a potential data race in your code, so while that "kicks the can further down" in the sense that you have to do more work, you may have actually found a potential bug[1].
All that is to say: At least give it a try, it might look like boilerplate work, but there's also a chance it points to something you will want to address anyway.
Of course it's possible that you had already seen that and mitigated with traditional means of ensuring data race safety, but in my experience chances are that it was an overlooked aspect like referencing a instance of a class with mutable state in there ↩︎