I have a simple wrapper class around WCSession to allow for easier unit testing. I'm trying to update it to Swift 6 concurrency standards, but running into some issues. One of them is in the sendMessage
function (docs here
It takes [String: Any] as a param, and returns them as the reply. Here's my code that calls this:
@discardableResult
public func sendMessage(_ message: [String: Any]) async throws -> [String: Any] {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<[String: Any], Error>) in
wcSession.sendMessage(message) { response in
continuation.resume(returning: response) // ERROR HERE
} errorHandler: { error in
continuation.resume(throwing: error)
}
}
}
However, I get this error:
Sending 'response' risks causing data races; this is an error in the Swift 6 language mode
Which I think is because Any
is not Sendable. I tried casting [String: Any]
to [String: any Sendable]
but then it says:
Conditional cast from '[String : Any]' to '[String : any Sendable]' always succeeds
Any ideas on how to get this to work?
https://developer.apple.com/forums/thread/765375
A key goal of Swift concurrency is that the compiler can check that your concurrent code follows all the rules. For that to work properly, the compiler needs type information. Any time you erase the types, like working with Any
values, things get tricky. This is problematic because many older APIs, those with an Objective-C heritage, work with Any
types all the time.
In this specific case, the Any
values in the dictionaries used by sendMessage(_:replyHandler:errorHandler:)
are a bit of a lie. You can’t transport any value over this connection. The values must be property list serialisable. And that’s a key hint for how to approach problems like this.
The folks who call sendMessage(…)
don’t send any data across the connection. They send specific requests and expect back specific responses. So I’d tackle this problem by making these requests and responses explicit. For example:
enum MyRequest {
case a
case b
case c
var dictionary: [String: Any] {
… your code here …
}
}
enum MyResponse {
case x
case y
case z
init?(dictionary: [String: Any]) {
… your code here …
}
}
@discardableResult
func sendMessageQ(_ request: MyRequest) async throws -> MyResponse {
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<MyResponse, Error>) in
wcSession.sendMessage(request.dictionary) { responseDict in
guard let response = MyResponse(dictionary: responseDict) else {
continuation.resume(throwing: …)
return
}
continuation.resume(returning: response) // ERROR HERE
} errorHandler: { error in
continuation.resume(throwing: error)
}
}
}
This has a couple of advantages:
-
It fixes your problem, because
MyRequest
andMyResponse
are just enums, and hence sendable. -
And the dictionary serialisation and deserialisation code never crosses an isolation boundary.
-
It centralises your parsing of network data. This is important because you can’t ‘trust’ data coming in from the network, and thus you want to check it as you receive it. Your current approach does this checking in many places — all the call site for your
sendMessage(_:)
routine — and that’s less than ideal.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"