I'm running into a weird SwiftUI concurrency issue that only seems to happen on a real device (iOS 18.5 in my case), and not in simulator. I have a NavigationSplitView
where the Detail view uses .task(id:_:)
to perform some async work upon appearance (in my real-world case, a map snapshot). When running this on a real device, the first execution of the task works as expected. However, any task subsequent executions, even ones for the same id, always start in the Task.isCancelled
state.
Is there something about .task(id:_:)
that I'm misunderstanding, or have I stumbled upon a serious bug here?
import SwiftUI
struct ContentView: View {
var body: some View {
TaskBugSplitView()
}
}
struct TestItem: Identifiable, Hashable {
var id: Int
var name: String
}
struct TaskBugSplitView: View {
@State
private var selectedItemIndex: [TestItem].Index?
@State
private var testItems: [TestItem] = [
TestItem(id: 1, name: "First Item"),
TestItem(id: 2, name: "Second Item"),
TestItem(id: 3, name: "Third Item"),
TestItem(id: 4, name: "Fourth Item"),
TestItem(id: 5, name: "Fifth Item")
]
var body: some View {
NavigationSplitView {
List(testItems.indices, id: \.self, selection: $selectedItemIndex) { item in
Text(testItems[item].name)
}
} detail: {
if let selectedItemIndex {
TaskBugDetailView(item: testItems[selectedItemIndex])
} else {
Text("Select an item")
.foregroundStyle(.secondary)
}
}
}
}
struct TaskBugDetailView: View {
@State var item: TestItem
@State private var taskResult = "Not started"
var body: some View {
VStack(spacing: 20) {
Text("Item: \(item.name)")
.font(.title2)
Text("Task Result:")
.font(.headline)
Text(taskResult)
.foregroundStyle(taskResult.contains("CANCELLED") ? .red : .primary)
Spacer()
}
.padding()
.navigationTitle(item.name)
.task(id: item.id) {
// BUG: On real devices, Task.isCancelled is true immediately for all tasks
// after the first successful one, even though the ID has changed
if Task.isCancelled {
taskResult = "CANCELLED at start"
print("Task cancelled at start for item \(item.id)")
return
}
taskResult = "SUCCESS"
print("Task completed successfully for item \(item.id)")
}
}
}