The case:
class ApplicationDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
private let cloudAssistant = CloudAssistant.shared
// MARK: - UIApplicationDelegate
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
print("✅fetching changes")
Task {
try await cloudAssistant.fetchAllChangesAsync()
}
}
}
@MainActor
class CloudAssistant: ObservableObject {
static let shared = CloudAssistant()
func fetchAllChangesAsync() async throws {
defer {
// some defer info, I think it is not important here
}
print("start fetching")
// here async functions to fetch changes from cloudkit
print("start saving")
// here await functions to update core data database
print("end saving")
}
}
But when more than 2 notifications appears, then it look like this:
✅fetching changes ✅fetching changes ✅fetching changes start fetching start fetching start saving end saving start fetching start saving start saving end saving end saving
and it is totally not what I need. Is there a way to achieve the order like:
✅fetching changes ✅fetching changes ✅fetching changes start fetching start saving end saving start fetching start saving end saving start fetching start saving end saving
There are a variety of approaches:
Await prior task:
@MainActor
class CloudAssistant: ObservableObject {
static let shared = CloudAssistant()
private var previousTask: Task<Void, Error>?
func fetchAllChanges() async throws {
let task = Task { [previousTask] in
try await previousTask?.value // let the previous task (if any) finish
try await fetchAllChangesAsync() // now call routine that does the work
}
self.previousTask = task
try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
// your previous implementation, but `private`
private func fetchAllChangesAsync() async throws {…}
}
Use an AsyncChannel
(from Apple‘s Swift Async Algorithms) to treat the notifications as a sequence:
@MainActor
class CloudAssistant: ObservableObject {
static let shared = CloudAssistant()
private let channel = AsyncChannel<Void>()
// called once during startup to monitor the channel
func monitorChannel() async {
for await _ in channel {
try? await fetchAllChangesAsync()
}
}
// called by app delegate when a notification comes in
func fetchAllChanges() async {
await channel.send(())
}
// your previous implementation, but `private`
private func fetchAllChangesAsync() async throws {…}
}
Adopt cancelation patterns, namely cancel the prior task:
@MainActor
class CloudAssistant: ObservableObject {
static let shared = CloudAssistant()
func fetchAllChanges() async throws {
let task = Task { [previousTask] in
previousTask?.cancel() // stop prior task, if any
try? await previousTask?.value // give it a chance to respond to cancellation and stop
try await fetchAllChangesAsync() // now call routine that does the work
}
self.previousTask = task
try await withTaskCancellationHandler {
try await task.value
} onCancel: {
task.cancel()
}
}
// your previous implementation, but `private`
func fetchAllChangesAsync() async throws {…}
}