@Sebi Pudilic and I recently begun working with Swift Concurrency. So far, I've successfully converted delegate methods into Swift asynchronous concurrency. We've created a class called GenericContinuationWrapper, which serves as a wrapper for transforming delegates into async await.
We need a way to make sure that the resume is called one time even if the user taps multiple times on a button that executes the performAction function.
We only have a task for timeout and do not know how to cancel the main task and to resumeWithFailure in case the continuation is resumed multiple times.
We are currently at a stage where we need to implement a timeout mechanism for a continuation to resume itself if the delegate isn't called for some reason (We encountered this while working with Bluetooth).
To achieve this, We've set up a task that sleeps for 3 seconds before resuming the continuation with an error.
func performAction(timeout: TimeInterval = 10, operation: () -> Void) async throws -> T {
operation() // Trigger the delegate-based operation
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
self.timeoutTask = Task {
do {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
if let timeoutTask = self.timeoutTask, !timeoutTask.isCancelled {
self.resumeWithFailure(withError: SomeError.bluetoothOperationFailed)
} else {
self.resumeWithFailure(withError: SomeError.bluetoothOperationFailed)
}
} catch {
self.resumeWithFailure(withError: SomeError.bluetoothOperationFailed)
}
}
}
}
When using Task, We encountered the error "Swift Task Continuation misused." In the event that a delegate is not called, the timer fires, and the continuation resumes with that error. We're curious as to why this is the case.
How can we approach this differently to make sure no leaks are received on a timeout.
This is our full code.
class GenericContinuationWrapper<T> {
typealias Continuation = CheckedContinuation<T, Error>
private var continuation: Continuation?
/// Safety timeout (not recommended to let continuation hang)
private var timeoutTask: Task<Void, Never>?
/// This function wraps the delegate call into an async function
func performAction(timeout: TimeInterval = 10, operation: () -> Void) async throws -> T {
operation() // Trigger the delegate-based operation
return try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
self.timeoutTask = Task {
do {
try await Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000))
if let timeoutTask = self.timeoutTask, !timeoutTask.isCancelled {
self.resumeWithFailure(withError: SomeError.bluetoothOperationFailed)
} else {
self.resumeWithFailure(withError: SomeError.bluetoothOperationFailed)
}
} catch {
self.resumeWithFailure(withError: SomeError.bluetoothOperationFailed)
}
}
}
}
/// Function to be called by the delegate method on success
func resumeWithSuccess(withResult result: T) {
timeoutTask?.cancel()
resumeContinuationWithSuccess(result)
}
/// Function to be called by the delegate method on failure
func resumeWithFailure(withError error: Error) {
timeoutTask?.cancel()
resumeContinuationWithFailure(error)
}
/// Resuming the continuation with a result
private func resumeContinuationWithSuccess(_ result: T) {
continuation?.resume(returning: result)
self.removeContinuation()
}
/// Resuming the continuation with an error
private func resumeContinuationWithFailure(_ error: Error) {
continuation?.resume(throwing: error)
self.removeContinuation()
}
/// Should call this when we finish working with this instance of self
func removeContinuation() {
self.continuation = nil
self.timeoutTask?.cancel()
self.timeoutTask = nil
}
}