I'm currently working on a small Swift 5/iOS 18/Xcode 16 app that starts an AVCaptureSession and displays the camera's image:
//Regular class linked to my VC
class ViewController: UIViewController, AVCapturePhotoCaptureDelegate {
...
private func configCamera() {
...
Task(priority: .background) {
session.startRunning()
//Update UI here
}
}
}
I know that session.startRunning() has to run on a background thread but when I run this code, it complains that it's running on the main thread and the UI updates as expected. I already tried to use Task.detached(priority: .background) instead but it's still running on the main thread.
If I use DispatchQueue.global(qos: .background).async (only tested without UI changes), then it doesn't complain but afaik you should use Tasks for new projects now.
The weird thing is: Apple's example project (Xcode 15 project, iOS 18) also uses Task(priority: .background) but there's no warning.
How do I properly switch to the background thread to start the session and then, once it's done, switch back to the main thread to update the UI? If switching to the background thread already isn't working, then I probably can't rely on await MainActor.run to correctly do its thing either, right?
Unfortunately AVFoundation still hasn't embraced Swift concurrency, so integrating the two is extremely awkward. There's not really a good, fully safe way to connect to two, given you need to interact with multiple isolations simultaneously. I suggest you watch the Build a responsive camera app that launches quickly session for a bit of guidance here.
Your fundamental issue is that Task inherits its isolation, so calling it from something on @MainActor will stay there. If nothing else you can do something like
This should throw the startRunning into the background.
Personally, I've been experimenting with a custom global AVActor that shares its executor with the AVSession delegate, so you can apply it to a class that is the delegate and then call back to the main queue for any UI updates you need, as well as sharing the AVCaptureVideoPreviewLayer. This also allows any processing to occur on the custom executor rather than the Swift concurrency pool, keeping things a bit more open for other work.
Edit: You'll also want to check your project settings for what Swift mode you're running in and whether you have default actor isolation or approachable concurrency enabled, so you can better predict where stuff will run.
Are you asking if you can inspect the isolation of a Task instance at runtime? There's no way to do this, and I'm having a difficult time imagining a scenario where it would be useful. Did you have a use in mind?
Hadn't realized you could use nonisolated like that. When was it added? From what I can tell it was part of the closure isolation control pitch, but it was part of Swift 6.1?
#isolation resolves to the current static isolation, which could be nil (representing nonisolated). It's a very useful tool for inspecting the isolation a compile-time.
Your code prints "undefined" at all the points in this program that are executing from a nonisolated function. Two settings that will have a major impact on this behavior are default isolation and NonisolatedNonsendingByDefault. Perhaps that explains why you aren't getting what you expect?
As Mattie already explained, if you have neither the NonisolatedNonsendingByDefault flag enabled nor @MainActor default isolation, a non-isolated asynchronous function will not run on the calling context's actor. You can, however, manually opt such functions into dynamic isolation with the nonisolated(nonsending) keyword.
Dynamic isolation also works with the #isolation macro, although it's somewhat awkward to use.
@freestanding(expression)
macro isolation<T>() -> T
Overview
If the type annotation provided for #isolation is not (any Actor)?, the type must match the enclosing actor type. If no type annotation is provided, the type defaults to (any Actor)?.
I'm new to the whole actor thing and even though I think I kind of understand the main bits, it's still really confusing to me, like that Task(priority: .background) isn't guaranteed to always run on a background thread (what's the point then?). I didn't specifically make the VC a @MainActor, does Xcode set it automatically?
Going to try your suggestion, thanks! But why does it run it on a background thread? I thought that async isn't guaranteed to do that.
Sorry, there's a lot of new information in your "personally" paragraph. I've only got a single class, the VC, and it handles the camera, its UI and also calls the next VC to display the result.
Where can I find the build mode? In "Targets > Build Settings > Swift Compiler - Language" there's only the option "Swift Language Version" and it's set to "Swift 5". In "Projects > Build Settings" the same setting says "Unspecified" (???). I didn't find a "Swift Mode".
I'm not 100% sure (someone please correct me if I'm wrong!) but my understanding is that if you set the priority of a task, that just controls its priority relative to other tasks on the same executor, not which thread it actually runs on. (With the caveat, of course, that someone could write a custom executor that uses different threads for different-priority jobs -- but @MainActor definitely doesn't do that.)
UIViewController is @MainActor, and actor isolation is inherited by subclasses by default. I think in modern Swift you can suppress this with nonisolated class, but you probably don't want this on a UI class where most things it does have to be on the main thread anyways.
That is relevant information. "Swift language mode" refers to the language version your code is being built with, as opposed to the version of the compiler you have installed; a Swift 6 compiler can still build in a Swift 5 language mode. The settings under "Swift Compiler - Concurrency", and some of the settings under "Swift Compiler - Upcoming Features" can also be relevant for concurrency-related problems, but I don't think any of them would change the fact that that task is inferred as @MainActor.