😺
【Swift Concurrency】withTaskCancellationHandlerについて
概要
Swift Concurrencyにおけるキャンセル時の処理は少々複雑だとは思いますが、そんな中withTaskCancellationHandler
は割と手軽にキャンセル時の処理を行うことができます。
@backDeployed(before: macOS 13.3, iOS 16.4, watchOS 9.4, tvOS 16.4)
func withTaskCancellationHandler<T>(
operation: () async throws -> T,
onCancel handler: () -> Void
) async rethrows -> T
operation
の実行中にキャンセルが発生した場合、onCancel
を呼び出し、キャンセル時の処理を行うことができます。以下のコードではTask
を定数task
に保持し、1秒後にキャンセルしています。
Task {
let task = Task {
await withTaskCancellationHandler {
let data = await fetchImageData()
print(data as Any)
} onCancel: {
print("called onCancel Closure")
}
}
try? await Task.sleep(for: .seconds(1))
task.cancel()
}
// 4Kの画像を取得するため、多くの場合1秒以上かかります。
func fetchImageData() async -> Data? {
guard let url = URL(string: "https://picsum.photos/3840/2160"),
let (data, _) = try? await URLSession.shared.data(from: url) else {
return nil
}
return data
}
出力は以下の通りです。
called onCancel Closure
nil
fetchImageData
の実行中にtask
がキャンセルされ、onCancel
が呼ばれます。その後にスコープを抜ける処理などは記述していないため、print(data as Any)
も呼ばれ、nil
が出力されています。
注意点
withTaskCancellationHandler
は、すでにキャンセルされたタスク内でも呼び出される可能性があります。そのような場合でも、withTaskCancellationHandler
は実行されます。この場合、onCancel
が実行され、その後operation
が実行されることに注意が必要です。
以下のコードは、キャンセルされたタスク内でwithTaskCancellationHandler
を呼び出したサンプルコードです。
Task {
let task = Task {
try? await Task.sleep(for: .seconds(1))
print("Task.isCancelled:", Task.isCancelled)
await withTaskCancellationHandler {
print("called operation closure")
} onCancel: {
print("called onCancel closure")
}
}
task.cancel()
}
出力結果は以下の通りです。
Task.isCancelled: true
called onCancel closure
called operation closure
出力結果から分かる通り、すでにキャンセルされているタスクではonCancel
が呼び出され、その後にoperation
が呼び出されていることが確認できます。
※ 最初の出力はきちんとタスクがキャンセルされているか確認するためのものです。
公式ドキュメント
Discussion