Michcio
Michcio

Reputation: 2866

Swift concurrency multiple await for single async result

I have AuthorizationRequester which can be call from many places simultaneously, but only first call can run requestAuthorizationFromUser() (and wait dozen of seconds for user interaction) - rest of these calls should await for result from requestAuthorizationFromUser(), but can't call it directly.

Look at the code:

actor AuthorizationRequester {
    enum SimpleResult {
        case success, failure
    }
    
    typealias ResultCompletion = (SimpleResult) -> ()
    
    private var requestInProgress = false
    private var requestCompletions = [ResultCompletion]()
    
    func request(completion: @escaping ResultCompletion) async {
        requestCompletions.append(completion)
        
        guard !requestInProgress else { return }
        
        requestInProgress = true
        
        let result = await requestAuthorizationFromUser()
        
        requestCompletions.forEach { $0(result) }
        requestCompletions.removeAll()
        requestInProgress = false
    }
    
    private func requestAuthorizationFromUser() async -> SimpleResult {
        // ...some code
    }
}

Everything works, but I really don't like async combined with completion closure :)

Is there any possibility to rewrite this function to version with header func request() async -> SimpleResult and same functionality?

Upvotes: 4

Views: 1484

Answers (1)

Rob
Rob

Reputation: 437542

You can save multiple calls to an async method await the same Task associated with the authorization request:

actor AuthorizationRequester {
    private var task: Task<SimpleResult, Never>?

    func request() async -> SimpleResult {
        if task == nil {
            task = Task { await requestAuthorizationFromUser() }
        }

        return await task!.value
    }

    private func requestAuthorizationFromUser() async -> SimpleResult {…}
}

We do not have to maintain the closures ourselves, but rather just just deal with Task objects and let Swift concurrency handle it from there.

And if you want to reset it again when the Task finishes (like your sample effectively does), you can set it to nil when done:

actor AuthorizationRequester {
    private var previousTask: Task<SimpleResult, Never>?

    func request() async -> SimpleResult {
        let task: Task<SimpleResult, Never>

        if let previousTask {
            task = previousTask
        } else {
            task = Task { await requestAuthorizationFromUser() }
            previousTask = task
        }

        return await withTaskCancellationHandler {
            defer { previousTask = nil }
            return await task.value
        } onCancel: {
            task.cancel()
        }
    }

    …
}

Note, because this is unstructured concurrency (where we bear the burden for handling cancelation), we should wrap this in withTaskCancellationHandler. I presume you are likely not contemplating the cancelation of this authorization at this point, but as a practical matter, whenever writing unstructured concurrency, we should make sure we support cancelation.

Upvotes: 7

Related Questions