Approximate example of code I need to achieve using GCD:
func performTask(completion: (() -> ())?) {
doSomeAsyncTask { [weak self] isSuccess in
if isSuccess {
self?.handleResult(completion: completion)
} else {
completion?()
self?.handleResult(completion: nil)
}
}
}
func handleResult(completion: (() -> ())?) {
…
}
func doSomeAsyncTask(completion: ((Bool) -> ())?) {
//example of async task which can be successful or not
DispatchQueue.main.async {
completion?(Bool.random())
}
}
I want to rewrite it with async/await
but I don't know how to implement performTask
. Other methods:
func handleResult() async {
…
}
func doSomeAsyncTask() async -> Bool {
await withCheckedContinuation { checkedContinuation in
Task {
checkedContinuation.resume(returning: Bool.random())
}
}
}
Could you please explain how to implement performTask
? I can't understand how to deal with situation when sometimes method should call completion and sometimes not.
In your completion-handler rendition, if doSomeAsyncTask
returns false
, it is immediately calling the closure and then calling handleResult
with nil
for the completion handler. This is a very curious pattern. The only thing I can guess is that the intent is to call the completion handler immediately if doSomeAsyncTask
failed, but still call handleResult
regardless, but if successful, not to return until handleResult
is done.
If that is your intent, the Swift concurrency rendition might be:
@discardableResult
func performTask() async -> Bool {
let isSuccess = await doSomeAsyncTask()
if isSuccess {
await handleResult()
} else {
Task { await handleResult() }
}
return isSuccess
}
Note, I am returning the success or failure of doSomeAsyncTask
because, as a matter of best practice, you always want the caller to have the ability to know whether it succeeded or not (even if you do not currently care). So, I made it a @discardableResult
in case the caller does not currently care whether it succeeded or failed.
The other pattern would be to throw an error if unsuccessful (and the caller can try?
if it does not care about the success or failure at this point).
enum ProcessError: Error {
case failed
}
func performTask() async throws {
if await doSomeAsyncTask() {
await handleResult()
} else {
Task { await handleResult() }
throw ProcessError.failed
}
}
Having shown a literal translation of the code you provided, I might suggest a simpler pattern:
@discardableResult
func performTask() async -> Bool {
let isSuccess = await doSomeAsyncTask()
await handleResult()
return isSuccess
}
In the first two alternatives, above, I use unstructured concurrency to handle the scenario where doSomeAsyncTask
returned false
, and allow the function to return a result immediately, and then perform handleResult
asynchronously later. This (like the completion handler rendition) begs the question of how to handle cancelation. And the question is whether handleResult
is slow enough to justify that pattern, or whether it was a case of premature optimization. It seems exceedingly strange that one would want to await handleResult
in the success path and not in the failure path. This third and final example simplifies this logic. We do not know enough about the rationale for the original completion-handler rendition to answer that question at this point.