Skip to content

Instantly share code, notes, and snippets.

@huynguyencong
Last active December 20, 2024 11:08
Show Gist options
  • Save huynguyencong/abcb6d6b5a64bd34807a459a9d1d16be to your computer and use it in GitHub Desktop.
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 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