# Why can't I send an NSImage across actor boundaries?

**URL:** <https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199>\
**Category:** Using Swift\
**Tags:** concurrency\
**Created:** [November 24, 2024, 5:58pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199 "2024-11-24T17:58:44Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [November 24, 2024, 5:58pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/1 "2024-11-24T17:58:44Z")

</div>

Can anyone explain the logic of this to me?

I have a @MainActor class that handles downloading some stuff.

I want to be able to call it, let it make an image for me (if it has downloaded the appropriate resources) and send the image over.

The source class isn't going to change it after creation.

So - why is it an error for me to receive that as the result of an async function in a different isolation ?

I get that NSImage is not Sendable - but I don't get why I should care.

Is there some risk here that I have misunderstood?

---

<div class="post-metadata">

**Author:** ![jrose](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/jrose/32/408_2.png) [@jrose](https://forums.swift.org/u/jrose)\
**Post date:** [November 24, 2024, 6:59pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/2 "2024-11-24T18:59:56Z")

</div>

NSImage is mutable (unlike UIImage). So if you send it across actor boundaries, but the original actor keeps a reference to it, then one side could mutate it while the other side is trying to display it.

Dealing with this is what `sending` is for: “even though NSImage can’t _always_ be sent across actor boundaries, _this_ NSImage is safe because I’m giving up all references to it”. I confess I haven’t tried it in practice to see how well it works, though.

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [November 24, 2024, 8:48pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/3 "2024-11-24T20:48:20Z")

</div>

I hate that the language acts as nanny here.

I know that NSImage is mutable.  
How very dare you tell me that I can't be trusted with a mutable object!

anyway...

`sending` fixes things in Swift 6 language mode - thank you.

However I'm not going near swift 6 mode until I have no other option.

Any way to make the warning go away in Swift 5?  
Or do I just declare NSImage Sendable and YOLO my way out of it...

Kinda doubly absurd that Swift 5 gives me a warning about something that will be an error in Swift 6 - but it actually isn't an error in Swift 6!

 ![Screenshot 2024-11-24 at 20.49.54](https://global.discourse-cdn.com/swift/original/3X/8/8/887d25ac1f3db67c30b5edc6fa6d8cf80144f71d.png)

---

<div class="post-metadata">

**Author:** ![mattie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mattie/32/13267_2.png) [@mattie](https://forums.swift.org/u/mattie)\
**Post date:** [November 25, 2024, 12:20am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/4 "2024-11-25T00:20:59Z")

</div>

When faced with this problem myself, I've sometimes used `CGImage` which is `Sendable`. Unsure if that'll help here or not...

The `Sink` type does look tricky to use though. It is non-`Sendable` with an init method that can only be called on `MainActor`. This means it will be difficult to invoke `doStuff`. Have you considered making the whole `Sink` type `MainActor`? This would fix that issue and also avoid needing to transfer the `NSImage` across isolation boundaries.

---

<div class="post-metadata">

**Author:** ![vns](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/vns/32/37396_2.png) [@vns](https://forums.swift.org/u/vns)\
**Post date:** [November 25, 2024, 8:40am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/5 "2024-11-25T08:40:58Z")

</div>

I think turning on region-based isolation feature in Swift 5 mode should help. This is the one that helps compiler to reason better about isolations and resolves some warnings/errors that might be felt out of place. You still would need `sending` though.

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [November 25, 2024, 10:21am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/6 "2024-11-25T10:21:03Z")

</div>

@mattie Sink is just a minimal example to demonstrate the issue. Even to demonstrate something so trivial - it has to be contorted to create the Source in the init 🤦‍♂️

@vns region-based isolation???

I'm sure I'll find a clear explanation of how to enable it, and what it does in the swift language guide 🤣

Swift is such a mess. How has it got to a point where returning an object from a function requires two undocumented language features?

---

<div class="post-metadata">

**Author:** ![mattie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mattie/32/13267_2.png) [@mattie](https://forums.swift.org/u/mattie)\
**Post date:** [November 25, 2024, 10:28am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/7 "2024-11-25T10:28:28Z")

</div>

> [@ConfusedVorlon](#):
>
> Sink is just a minimal example to demonstrate the issue. Even to demonstrate something so trivial - it has to be contorted to create the Source in the init 🤦‍♂️

I think the problems you've run into with `NSImage` is a special-case of a more general problem stemming from how you've structured `Sink`'s isolation. Types like this will always be hard, and sometimes impossible, to use.

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [November 25, 2024, 10:31am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/8 "2024-11-25T10:31:09Z")

</div>

To my mind, a language where creating a class, then passing it to be used in a background thread is 'hard and sometimes impossible' is a very badly designed language.

The practical impact is that it just pushes devs to do everything on the main thread - which is obviously a fail.

Considering NSImage a special case chimes well with how I percieve the development of Swift.  
It's obviously a central class for anyone developing anything real on MacOS. But the 'science project' that is modern swift evolution doesn't seem to consider real usage important.

To be fair - using sending and enabling RBI has fixed this issue for me.  
Two undocumented swift features...

---

<div class="post-metadata">

**Author:** ![vns](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/vns/32/37396_2.png) [@vns](https://forums.swift.org/u/vns)\
**Post date:** [November 25, 2024, 10:35am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/9 "2024-11-25T10:35:30Z")

</div>

> [@ConfusedVorlon](#):
>
> I'm sure I'll find a clear explanation of how to enable it, and what it does in the swift language guide

You just use feature flag in compiler configuration with `RegionBasedIsolation` value. There is a detailed instruction how to use it depending on whether it is SPM or Xcode: [Swift.org - Using Upcoming Feature Flags](https://www.swift.org/blog/using-upcoming-feature-flags/)

Initial proposal is here: [swift-evolution/proposals/0414-region-based-isolation.md at main · swiftlang/swift-evolution · GitHub](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md)

And migration guide mentioning this: [Documentation](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety#Flow-Sensitive-Isolation-Analysis)

> [@ConfusedVorlon](#):
>
> Swift is such a mess. How has it got to a point where returning an object from a function requires two undocumented language features?

It is unfortunate that it is missing from documentation straight away. I think this worth probably filling an issue. However, this is not undocumented completely, you can find this in a migration guide that is made to help with transitioning to Swift 6.

Also, you are not simply returning object from a function, you pass it across concurrency domains, which could easily led to issues if missed. Yes, you here know what you do, but (1) not everybody can be so swift with concurrency logic; (2) we all can make mistake – and with concurrency that's quite easy – so this is a tradeoff to make code safer in long term.

* * *

Have to add that I extemelly agree with @mattie on the fact that this minimal example already seems to be hard to use (for example, `Sink` has to be created from the main actor, but cannot be passed anywhere since it is non-`Sendable`). We have had a long thread IIRC regarding this some time ago, so this is just one note 🙂

---

<div class="post-metadata">

**Author:** ![mattie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mattie/32/13267_2.png) [@mattie](https://forums.swift.org/u/mattie)\
**Post date:** [November 25, 2024, 10:41am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/10 "2024-11-25T10:41:22Z")

</div>

> [@ConfusedVorlon](#):
>
> To be fair - using sending and enabling RBI has fixed this issue for me.  
> Two undocumented swift features...

Here's some documentation on how Region-Based Isolation and `sending` works:

[https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety#Flow-Sensitive-Isolation-Analysis](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/dataracesafety#Flow-Sensitive-Isolation-Analysis)

---

<div class="post-metadata">

**Author:** ![mattie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mattie/32/13267_2.png) [@mattie](https://forums.swift.org/u/mattie)\
**Post date:** [November 25, 2024, 10:53am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/11 "2024-11-25T10:53:15Z")

</div>

> [@ConfusedVorlon](#):
>
> Considering NSImage a special case chimes well with how I percieve the development of Swift.  
> It's obviously a central class for anyone developing anything real on MacOS. But the 'science project' that is modern swift evolution doesn't seem to consider real usage important.

No no, you misunderstand what I meant! Your example using `NSImage` here is just a **particular** example of a very common issue. You are using an isolation pattern that is known to be a problem. That is making it hard for you to work with the non-`Sendable` types involved (`Sink` and `NSImage`). But they are not special at all. Using `sending` or relying on region-based isolation is a workaround for the underlying issue.

Any time you use this isolation pattern (which I've come to call "split isolation") you will run into problems.

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [November 25, 2024, 11:04am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/12 "2024-11-25T11:04:22Z")

</div>

re documentation - the Language Reference doesn't even have the word sendable.

I get that this stuff is in evolution proposals. But I don't think those can be called documentation.

I was incorrect earlier - RBI + sending seems to only partially fix this.  
It works in my test project - but not in my real one. Possibly because the result is crossing a package boundary???

to the specific case. This is the problem I'm trying to solve:

My app generates videos for a dropzone.  
That dropzone has a bunch of branding (images/videos).  
The source of truth is the web server - so I have a @MainActor class which fetches updates from the web, and handles downloading the latest assets.

This could be some other defined Actor - but it needs to be _something_ and MainActor is fine.

So - I can call my AssetManager and ask for an image asynchronously.  
It loads the correct image from the disk, or if necessary, uses an async semaphore to run a single download, save and return the asset.

Now I want to use that image in the rendering process which I want to happen on a background thread.

So - how should I avoid 'split isolation' ?

---

<div class="post-metadata">

**Author:** ![mattie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mattie/32/13267_2.png) [@mattie](https://forums.swift.org/u/mattie)\
**Post date:** [November 25, 2024, 11:56am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/13 "2024-11-25T11:56:01Z")

</div>

The general idea is: don't isolate _members_ of a type, isolate the entire thing.

I'm not sure I'd go so far as to call this a rule, because I'm sure there are valid uses for types that have mixed isolation, especially if the type is `Sendable`.

But, people often arrive at this arrangement while trying to get rid of compiler warnings, especially around protocol conformances. If that was the case here, take a look at that migration guide too, because there's quite a lot of discussion around that topic.

Something I've done in the past is work with `CGImage` instances in the background because they are `Sendable`, but do all my storage/caching on the `MainActor`. The doesn't sound all that far off from what you currently have and might be worth taking a look at.

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [November 25, 2024, 12:52pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/14 "2024-11-25T12:52:36Z")

</div>

> The general idea is: don't isolate _members_ of a type, isolate the entire thing.

I think I'm following that here.  
My AssetManager is entirely on @MainActor

One of the things it produces is an NSImage which I want to use in different Type in another isolation. (to do my expensive rendering work)

The RenderingType gets the image by an async call  
let image = await assetManager.latestLogo()

I assume you're not suggesting that any type which interacts with AssetManager should itself be MainActor too?

Or are you?

---

<div class="post-metadata">

**Author:** ![OneSadCookie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/onesadcookie/32/3205_2.png) [@OneSadCookie](https://forums.swift.org/u/OneSadCookie)\
**Post date:** [December 1, 2024, 9:34am UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/15 "2024-12-01T09:34:11Z")

</div>

- 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`.

```swift
// 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:

```swift
@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:

```swift
@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:

```swift
@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:

```swift
// 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)
}

```

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [December 1, 2024, 12:48pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/16 "2024-12-01T12:48:12Z")

</div>

Hi Keith,

Thanks for the detailed response.

I'm already on 16.2 in Swift 5 mode, so that isn't the issue.

Looking more closely - perhaps the error is that I'm returning an NSImage? rather than an NSImage

 ![Screenshot 2024-12-01 at 12.42.52](https://global.discourse-cdn.com/swift/original/3X/e/4/e4851040b7ddd7cd43d5e09e1450340e10ee1f9e.png)

Obviously my mistake for expecting esoteric language features like Optional would be handled sanely 🤦‍♂️

At what point is the answer to nuke from orbit and start again ???

Re your suggestion to move to CGImage - I'm going to keep fighting the fight of believing that the compiler works for me and not the other way around.

NSImage is the natural object for me to return - I don't want to start contorting my code to to make the compiler happy when fundamentally it is the compiler that is wrong.

---

<div class="post-metadata">

**Author:** ![dima\_kozhinov](https://avatars.discourse-cdn.com/v4/letter/d/258eb7/32.png) [@dima\_kozhinov](https://forums.swift.org/u/dima_kozhinov)\
**Post date:** [December 1, 2024, 2:13pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/17 "2024-12-01T14:13:01Z")

</div>

Um... **H** SImage? Is that a typo in the compiler?

---

<div class="post-metadata">

**Author:** ![taylorswift](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/taylorswift/32/43413_2.png) [@taylorswift](https://forums.swift.org/u/taylorswift)\
**Post date:** [December 1, 2024, 3:40pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/18 "2024-12-01T15:40:26Z")

</div>

it has long puzzled me why a feature (RBI) that has had so much work invested into it and is so critical to the success of Swift 6 has such sparse documentation…

---

<div class="post-metadata">

**Author:** ![ConfusedVorlon](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/confusedvorlon/32/14854_2.png) [@ConfusedVorlon](https://forums.swift.org/u/ConfusedVorlon)\
**Post date:** [December 1, 2024, 3:55pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/19 "2024-12-01T15:55:16Z")

</div>

> [@dima\_kozhinov](#):
>
> Um... **H** SImage? Is that a typo in the compiler?

Just a crossplatform typealias. On MacOS, it is NSImage, on iOS, UIImage  
The two are so similar that I can mostly use them interchangeably.

---

<div class="post-metadata">

**Author:** ![OneSadCookie](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/onesadcookie/32/3205_2.png) [@OneSadCookie](https://forums.swift.org/u/OneSadCookie)\
**Post date:** [December 1, 2024, 11:43pm UTC](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199/20 "2024-12-01T23:43:09Z")

</div>

> [@ConfusedVorlon](#):
>
> Obviously my mistake for expecting esoteric language features like Optional would be handled sanely 🤦‍♂️

`Optional` is irrelevant; `Optional<T>` is `Sendable` iff `T` is `Sendable`. Region-based isolation doesn't care about the specifics of the types, only if they're `Sendable`.

This works fine:

```swift
// package A
@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)
    }
}

// package B
nonisolated func processAnImage(vendor: ImageVendor) async {
    guard let image = await vendor.gimmeAnImage() else { return }
    process(image)
}

```

[Next page](https://forums.swift.org/t/why-cant-i-send-an-nsimage-across-actor-boundaries/76199.md?page=2)
