I'm encountering an error message "Task or actor isolated value cannot be sent" in my EmotionDetectorViewModel class. I'm using Swift concurrency to process video frames from the front camera and detect emotions using a Core ML model.
The error occurs specifically on the line currentBuffer = pixelBuffer. I'm trying to capture the incoming pixelBuffer from the camera output and store it in a class property currentBuffer for further processing. However, it seems like I can't assign a value captured in a non-isolated context (@MainActor) to a property.
I've searched online and found discussions mentioning that CVPixelBuffer is not Sendable. Could this be the reason for the error? If so, what's the recommended approach to handle this scenario while adhering to Swift concurrency best practices?
I've been following the guidelines for Swift concurrency but am stuck at this point. Any help would be greatly appreciated!
Does processCurrentImage really belong on the main actor? That strikes me as precisely the sort of thing that one wants off the main actor. It is probably only the end result of that process (what the emotion was, the relevant coordinates, etc.) that you want back on the main actor. But not the processing of the image, itself…
Just keep in mind that an @preconcurerncy import is basically a file-wide assertion that you are using the imported framework correctly. I'm not familiar enough with it to know if that's true or not.
Here's a slightly simplified version of your code that I think is representative and produces the same error:
@MainActor
class SomeView {
init() {}
}
class Foo {
let view: SomeView? = nil
func foo() {
// error: task or actor isolated value cannot be sent
Task { @MainActor in
view
}
}
}
It's hard to understand what's going on here because the compiler's error message doesn't directly point to the problem. But if we can further simplify and that will make the problem much more clear:
class Foo {
func foo() {
// error: type 'Foo' does not conform to the 'Sendable' protocol
Task { @MainActor in
self
}
}
}
And this explains why the @preconcurrency import wasn't helping. The problem has nothing to do with the STTextView type. The issue is with Foo!
Types that aren't Sendable are pretty hard to use with concurrency features, like that Task here. It's possible, but can require some pretty complex stuff to make it work.
Given that Foo here contains MainActor-only state, one option that often works well is to isolate the whole type.
@MainActor
class SomeView {
init() {}
}
@MainActor // <- global actor isolation makes the type Sendable
class Foo {
let view: SomeView? = nil
func foo() {
Task { @MainActor in
view
}
}
}
(In fact, because now the foo method is MainActor as well, the explicit @MainActor in for the closure is no longer necessary. But it isn't wrong, so I just left it in to match the original version.)
that is interesting, because it sometimes works. By sometimes, I mean that I can see that it doesn't work when "SomeView" is declared in another module
That’s because types can be automatically marked Sendable within the same module if it can be inferred, but you have to manually mark them across module boundaries, since it changes the public interface in a way you may not want to guarantee (kind of like the default init’s for structs not being public).
Yes, it's true that using an explicit capture can work around this problem if the value captured is sendable. That's a useful trick!
But it can also get you into trouble, because it allows you to go further down the this path. The Foo type remains difficult to use. I have a hard time imagining how and where you would need to store and reference mutable MainActor state in a type that isn't also itself ultimately created on and used from the MainActor.
I'm not saying using a capture is wrong. Or that this type doesn't represent a reasonable real-world problem. But I do think it is worth caution here, because (today anyways) async methods or Task within a non-Sendable is very hard.
What does the definition of SomeView look like in that other module?