swiftgrand-central-dispatchcompletionswift-concurrency

Convert code with GCD to async/await in Swift?


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.


Solution

  • 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.