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
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.