- Use Xcode 16, so you get the "swift 6 compiler", which you'll use in "Swift 5 mode" (please don't shoot me, I'm just the messenger). This will give you access to
sending
.
// package A
@MainActor
final class ImageVendor {
func gimmeAnImage() -> sending NSImage {
NSImage()
}
}
// package B
nonisolated func processAnImage(vendor: ImageVendor) async {
let image = await vendor.gimmeAnImage()
process(image)
}
This works fine. Leave off sending
though, and you'll get:
error: non-sendable type 'NSImage' returned by call to main actor-isolated function cannot cross actor boundary
error: non-sendable type 'NSImage' returned by call to main actor-isolated function cannot cross actor boundary
You can still create problems for yourself, though; if you try to return an existing image, you'll end up in trouble:
@MainActor
final class ImageVendor {
var image = NSImage()
func gimmeAnImage() -> sending NSImage {
image
}
}
error: sending 'self.image' risks causing data races
note: main actor-isolated 'self.image' cannot be a 'sending' result. main actor-isolated uses may race with caller uses
The right way to fix this should be with NSCopying
, but unfortunately that also doesn't work, this time with a very unhelpful error:
@MainActor
final class ImageVendor {
var image = NSImage()
func gimmeAnImage() -> sending NSImage {
image.copy() as! NSImage
}
}
error: task or actor isolated value cannot be sent
This is because Swift has to treat the return value of copy
as being in the same isolation zone as the receiver. I'd consider it a bug that Swift doesn't understand NSCopying
here, but it is what it is. So we need to actually find a way to create a new NSImage instance that doesn't refer to the old one.
This is also where we run out of good solutions. This will work for some kinds of image:
@MainActor
final class ImageVendor {
var image = NSImage()
func gimmeAnImage() -> sending NSImage {
let cgImage = image.cgImage(
forProposedRect: nil,
context: nil,
hints: nil
)!
return NSImage(cgImage: cgImage, size: image.size)
}
}
But now that we're trafficking through CGImage
anyway, I'd suggest using CGImage
instead of NSImage
in the first place — it's Sendable
, and you can just write what you wanted to all along:
// package A
@MainActor
final class ImageVendor {
var image = someCGImage()
func gimmeAnImage() -> CGImage {
return image
}
}
// package B
nonisolated func processAnImage(vendor: ImageVendor) async {
let image = await vendor.gimmeAnImage()
process(image)
}