Search code examples
swiftswiftuiconcurrency

SwiftUI .task view modifier: in which thread does it run?


SwiftUI has this .task(priority:_:) view modifier. It runs async code. The default priority is userInitiated. There are no mentions to which thread this runs on.

Just by testing seems that runs on background threads, but: is it safe to assume that always runs in a non-main thread?


Solution

  • I am not sure how you concluded that this was not on the main thread, but when I run it on a debugger, it is on the main queue:

    enter image description here

    And that makes sense, as body is isolated to the main actor, and I would expect its task to run on behalf of the current actor (the main actor), too. (If you need to get something off the current actor, you would generally use a detached task. You only need to do that for slow, synchronous units of work, though.) FWIW, contemporary versions of SwiftUI now isolate the whole View to the main actor, further reducing any ambiguity.

    Bottom line, the SwiftUI .task view modifier for body is isolated to the main actor. (FWIW, I verified this empirically in a number of ways, too.)

    In short, if you really need to make sure something is off the main actor (e.g., running something slow and synchronous within the .task view modifier), make sure to put that on a separate actor, within a detached task, or some equivalent pattern.


    A few other observations:

    1. As an aside, we no longer focus on threads within Swift concurrency. It can yield misleading results. Swift concurrency abstracts us away from thread-level reasoning. In the future, Swift 6 will remove many of these API that we used to programmatically query Thread information. Bottom line, stop worrying about threads and focus on actor isolation.

      If you really want to learn more about the Swift concurrency threading model, see WWDC 2021 video Swift concurrency: Behind the scenes. It does not directly touch upon the .task view modifier question, but it will help you appreciate why we no longer focus on threads, like we once may have.

    2. Personally, in my .task view modifiers, I am generally just calling some async method, and in that scenario, the actor used by the .task view modifier becomes largely irrelevant (because we just await that call, which suspends execution on the current actor, freeing it to go do other stuff).

    3. Likewise, back in the GCD world, we wasted a lot of mental energy worrying if we were calling some particular method from the right queue (and dealt with runtime errors if we messed up). But in Swift concurrency, we isolate the called function to the appropriate actor and now we get compile-time warnings if we do not call it correctly (e.g., double-checking Sendable types when we use the “Strict concurrency checking” build setting of “Complete”, neglect to include a needed await when we cross actor boundaries, etc.).


    In the comments, you mentioned that you were “checking the thread … inside the async function [called] from the task modifier”.

    Yep, SE-0338, implemented in Swift 5.7, informs us that a non-isolated async functions “do not run on any actor’s executor”. So, unless it was a function somehow explicitly isolated to the main actor, it would not run on the main actor.