I need to handle an asynchronous task result. The problem is if I call it when it is in progress I need to update completion.
In GCD
it will look somehow like that:
func someFunc(completion: (() -> ())?) {
if isLoading {
if let completion {
delayedCompletion = completion
}
return
}
isLoading = true
delayedCompletion = completion
//example of some async task
DispatchQueue.main.async { [weak self] in
self?.delayedContinuation?()
self?.delayedContinuation = nil
}
}
But how to do that with async/await
? Tried to write code:
func someFunc() async {
if isLoading {
return await withCheckedContinuation { [weak self] checkedContinuation in
self?.delayedContinuation = checkedContinuation
}
}
isLoading = true
return await withCheckedContinuation { [weak self] checkedContinuation in
self?.delayedContinuation = checkedContinuation
Task { @MainActor [weak self] in
self?.delayedContinuation?.resume()
}
}
}
Is it correct or are there other ways to add a varying completion
block?
There are a few basic patterns:
Await prior task.
actor AdManager {
var inProgressTask: Task<Void, Error>? // if you don’t `try` anything inside the `Task {…}`, this property would be `Task<Void, Never>?`
func nextAdWithWait() async throws {
if let inProgressTask {
try await inProgressTask.value
return
}
let task = Task {
defer { inProgressTask = nil }
try await fetchAndPresentAd()
}
inProgressTask = task
// note, because this is unstructured concurrency, we want to manually handle cancelation
try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
}
Cancel prior task and launch a new one.
func nextAdWithCancelPrevious() async throws {
inProgressTask?.cancel()
let task = Task {
defer { inProgressTask = nil }
try await fetchAndPresentAd()
}
inProgressTask = task
try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
Having shown a couple of basic patterns, you likely want to fetch ads and present them in the UI, so you want to decouple the fetching from the presentation in the UI.
One might generate an asynchronous sequence of the ads from some “ad manager” and yield values as ads are fetched. So the UI can initiate the “periodically fetch ads” and then process them as they come in.
actor AdManager {
/// Generate sequence of ads
func ads(durationBetweenAds: Duration = .seconds(60)) -> AsyncStream<Ad> {
AsyncStream { continuation in
let task = Task {
defer { continuation.finish() }
while !Task.isCancelled {
if let ad = await nextAd() {
continuation.yield(ad)
try? await Task.sleep(for: durationBetweenAds)
} else {
try? await Task.sleep(for: .seconds(10)) // in case ad server is down or broken, don't flood it with requests (but, at the same time, maybe not wait a full minute before you try again)
}
}
}
continuation.onTermination = { _ in
task.cancel()
}
}
}
func nextAd() async -> AdManager.Ad? {…}
}
extension AdManager {
/// model structure for a given ad … perhaps your ad platform already has a model object for this
struct Ad {…}
}
Then the UI can monitor this asynchronous sequence. E.g., in UIKit:
class ViewController: UIViewController {
private var adsTask: Task<Void, Never>?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
adsTask = Task { await showAds() }
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
adsTask?.cancel()
}
func showAds() async {
let adManager = await AdManager()
let ads = await adManager.ads()
for await ad in ads {
await showAdInUI(ad)
}
}
func showAdInUI(_ ad: AdManager.Ad) async {…}
}
In SwiftUI, you don’t need this unstructured concurrency. Just directly await
the showAds
function in a .task
view modifier, and it will start it when the view appears and will cancel it automatically when the view disappears. But, in UIKit, we need to manually handle cancelation, like above.
Now, you haven’t shared your ad framework, so many of the details above may vary. But don’t get lost in the details. The basic idea in Swift concurrency is that you likely want an asynchronous sequence of ads, and then let the UI iterate through this sequence and present them as they come in. This is the natural Swift concurrency pattern for consuming an asynchronous sequence of events.