Last active
December 20, 2024 11:08
-
-
Save huynguyencong/abcb6d6b5a64bd34807a459a9d1d16be to your computer and use it in GitHub Desktop.
Write async function for sync function which is cancellable with Swift concurrency
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This example demonstrates how to write an async function from a heavy synchronous function (`heavyTask`) | |
// that is also cancellable. | |
// | |
// **Swift Concurrency Version (`heavyTaskAsyncWithSwiftConcurrency`)** | |
// - It uses `Task.detached` to ensure the heavy task is executed on a separate thread. | |
// - To handle cancellation, `withTaskCancellationHandler` is used to capture the cancel event and propagate it | |
// to the detached task. | |
// | |
// **GCD Version (`heavyTaskAsyncWithGCD`)** | |
// - Inside the `withCheckedContinuation` closure, `Task.isCancelled` does not work. | |
// - As a workaround, `withTaskCancellationHandler` is used to set an `isCancelled` variable to `true` | |
// when the original task is cancelled. | |
import SwiftUI | |
struct ContentView: View { | |
var body: some View { | |
VStack { | |
Button("Tap here") { | |
buttonTapped() | |
} | |
} | |
.padding() | |
} | |
} | |
extension ContentView { | |
func buttonTapped() { | |
print("Button tapped; Is main thread? \(Thread.isMainThread)") | |
let task = Task { | |
await heavyTaskAsyncWithSwiftConcurrency() | |
} | |
// Cancel task after 1 second | |
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { | |
task.cancel() | |
} | |
} | |
nonisolated func heavyTask(isCancelled: () -> Bool) { | |
print("Starting heavy task: \(Date.now). Is main thread? \(Thread.isMainThread)") | |
let total = 20_000_000 | |
for i in 0...total { | |
if isCancelled() { | |
break | |
} | |
if i % 1_000_000 == 0 { | |
print("Doing task: \(Double(i)/Double(total)*100)%") | |
} | |
} | |
print("Done heavy task: \(Date.now). Is main thread? \(Thread.isMainThread)") | |
} | |
// Recommended Swift concurrency | |
func heavyTaskAsyncWithSwiftConcurrency() async { | |
// Use `Task.detached` to ensure the heavy task is executed on a separate thread | |
let task = Task.detached { | |
heavyTask(isCancelled: { Task.isCancelled }) | |
} | |
// Capture the cancel event and propagate it to the detached task | |
await withTaskCancellationHandler { | |
await task.value | |
} onCancel: { | |
task.cancel() | |
} | |
} | |
// Not recommended GCD version | |
func heavyTaskAsyncWithGCD() async { | |
var isCancelled = false | |
// To avoid warnings when using `isCancelled` directly in concurreny code | |
let isCancelledSetter = { | |
isCancelled = $0 | |
} | |
// To avoid warnings when using `isCancelled` directly in concurreny code | |
let isCancelledGetter = { | |
isCancelled | |
} | |
// Capture the cancel event and pass it to the `heavyTask` using `isCancelled` setter and getter | |
await withTaskCancellationHandler { | |
await withCheckedContinuation { continuation in | |
// Run the heavy task in a background thread | |
DispatchQueue.global().async { | |
heavyTask(isCancelled: isCancelledGetter) | |
continuation.resume() | |
} | |
} | |
} onCancel: { | |
isCancelledSetter(true) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment