I am running a background operation from a UIViewController, let's call it FirstViewController, by means of Task.detached(priority: .userInitiated). Via a few other non-UI classes this ends back up in another UIViewController, let's call it SecondViewController via call-backs. In the callback I call self.updateTheThing() on SecondViewController.
As func updateTheThing() is a function on a UIViewController and as such implicitly marked with @MainActor I would assume that calls to it will always execute on MainActor no matter how the call-chain arrives to it.
In reality I can see that on run-time we are still on a queue named com.apple.root.user-initiated-qos.cooperative (concurrent) when we get back to func updateTheThing().
Yes, inside the Task.detached {…}, you will be running on one of the threads from the cooperative thread pool. But that doesn’t mean that updateTheThing ran on the background thread, because it will run on the main thread, because, as you noted, that function is isolated to the main actor. But, yes, when you get back to the detached task, that will switch you back to the cooperative thread pool.
But, this begs the question as to why you used a detached task in the first place. You use detached tasks when you need to do something slow and synchronous. If you are calling something that takes a little time, but is asynchronous (i.e., you just await something), then you don’t need a detached task. A normal Task {…} would be sufficient. And when you hit an await in this time-consuming async function, the main thread is free to do other things (i.e., the main thread is not blocked) by virtue of actor reentrancy. The net effect is that the use-case for detached tasks is more rare than, for example, GCD’s dispatching to a global/background queue.
Anyway, if you are interested in the threading model underpinning Swift concurrency, I think that Swift concurrency: Behind the scenes is a good introductory video, if you haven’t see it already.
I have tried with Task { } also and with same result. The call-chain is much more complex and ends up in another UIViewController via some Rx stuff which then calls out to a func.
With a breakpoint I have verified that no dispatch queue stuff is involved, just one func call to the next. The place where updateTheThing() is called is an @escaping closure in the Rx package. So it is possible that all those indirections or the closure confuse the compiler enough that the @MainActor on func updateTheThing() is not respected.
I better see if I can reduce it to a manageable example.
If it's going through Rx you lose all concurrency guarantees and checking, so it's up to you to manage the queues on the Rx side, or map it into concurrency in a way that preserves the information you need.
You should not rely on the thread information. There is a reason that the Thread API is disabled in Swift concurrency contexts. FWIW, one potential issue is that the compiler does static analysis of your code during compilation and determines whether it really needs to be running on the main thread. So just because it is technically isolated to the main actor, there are all sorts of clever optimizations that might defy your threading expectations.
If you want to check whether you are successfully isolated to the main actor, use MainActor.assertIsolated(), instead. Put that inside the function that should be isolated to the main actor and see if it reports any issue.
Bottom line, stop worrying about threads and use MainActor.assertIsolated() to determine whether you are isolated to the main actor or not.
Now that having been said, if you are confident that this function really is isolated to the main actor, but that MainActor.assertIsolated() is failing, then I might suggest you post a more fulsome example and include details about what version of Xcode you are using.