Search code examples
swiftuiswift-concurrency

The example code of task modifier of SwiftUI is confusing


Here is the code in Apple developer document。

let url = URL(string: "https://example.com")!
@State private var message = "Loading..."

var body: some View {
    Text(message)
        .task {
            do {
                var receivedLines = [String]()
                for try await line in url.lines {
                    receivedLines.append(line)
                    message = "Received \(receivedLines.count) lines"
                }
            } catch {
                message = "Failed to load"
            }
        }
}

Why don't it update message in the UI thread as code below

DispatchQueue.main.async {
    message = "Received \(receivedLines.count) lines"
}

Does the code in task block alway run in the UI thread?

Here is my test code. It sometimes seems that task isn't inherit the actor context of its caller.

func wait() async {
    await Task.sleep(1000000000)
}

Thread.current.name = "Main thread"
print("Thread in top-level is \(Thread.current.name)")

Task {
    print("Thread in task before wait is \(Thread.current.name)")
    if Thread.current.name!.isEmpty {
        Thread.current.name = "Task thread"
        print("Change thread name \(Thread.current.name)")
    }
    await wait()
    print("Thread in task after wait is \(Thread.current.name)")

}

Thread.sleep(until: .now + 2)

// print as follow

// Thread in top-level is Optional("Main thread")
// Thread in task before wait is Optional("")
// Change thread name Optional("Task thread")
// Thread in task after wait is Optional("")

Thread in task is different before and after wait()

Thread in task is not Main thread


Solution

  • Great question! It looks like a bug, but in fact Apple's sample code is safe. But it is a safe for a sneaky reason.

    Open a Terminal window and run this:

    cd /Applications/Xcode.app
    find . -path */iPhoneOS.platform/*/SwiftUI.swiftmodule/arm64.swiftinterface
    

    The find command may take a while to finish, but it will eventually print a path like this:

    ./Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/SwiftUI.framework/Modules/SwiftUI.swiftmodule/arm64.swiftinterface
    

    Take a look at that swiftinterface file with less and search for func task. You'll find the true definition of the task modifier. I'll reproduce it here and line-wrap it to make it easier to read:

      @inlinable
      public func task(
        priority: _Concurrency.TaskPriority = .userInitiated,
        @_inheritActorContext
        _ action: @escaping @Sendable () async -> Swift.Void
      ) -> some SwiftUI.View {
        modifier(_TaskModifier(priority: priority, action: action))
      }
    

    Notice that the action argument has the @_inheritActorContext attribute. That is a private attribute, but the Underscored Attributes Reference in the Swift repository explains what it does:

    Marks that a @Sendable async closure argument should inherit the actor context (i.e. what actor it should be run on) based on the declaration site of the closure. This is different from the typical behavior, where the closure may be runnable anywhere unless its type specifically declares that it will run on a specific actor.

    So the task modifier's action closure inherits the actor context surrounding the use of the task modifier. The sample code uses the task modifier inside the body property of a View. You can also find the true declaration of the body property in that swiftinterface file:

      @SwiftUI.ViewBuilder @_Concurrency.MainActor(unsafe) var body: Self.Body { get }
    

    The body method has the MainActor attribute, which means it belongs to the MainActor context. MainActor runs on the main thread/queue. So using task inside body means the task closure also runs on the main thread/queue.