Reputation: 2866
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
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