5

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

  • Because task is performed in background queue, but UI needs to be updated in main queue. – Asperi Dec 10 '21 at 16:23
  • It could be that it is being done on background but. This is the issue we are all facing it seems that is behavior has gotten worse with iOS 15. Submit a bug report using feedback assistant. So many things are “fixed” in SwiftUI with that code. – lorem ipsum Dec 10 '21 at 16:24

1 Answers1

5

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.

rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • Refer to https://developer.apple.com/documentation/swift/task/3856790-init, I know the task inherits the priority and actor context of the caller. But I'm confused about [my test code](https://gist.github.com/MakHoCheung/26e3080407402a464ce6b8f47d6acb29). The result of the code is different running in different environments like playground, cli app and iOS app. Task is always running in a consistent thread in iOS app, but is not always running in a consistent thread in playground and cli app –  Dec 11 '21 at 04:07
  • I believe top-level code like your test code is not bound to the `MainActor` context, so your `Task` doesn't inherit the `MainActor` context. – rob mayoff Dec 11 '21 at 05:28
  • According to [SE-0323](https://github.com/apple/swift-evolution/blob/main/proposals/0323-async-main-semantics.md), if you move your top-level code into an `static func main() async` method on an `@main` type, then it will be bound to the `MainActor` context. Xcode-13.2 should include this behavior. – rob mayoff Dec 11 '21 at 05:34
  • That's it. Top-level code is special. Thank you for your help. –  Dec 11 '21 at 07:06