iosswiftswiftuiasync-await

How to wait until async function finish the job and then call it again from non async method?


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

Solution

  • There are a variety of approaches:

    1. 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 {…}
      }
      
    2. 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 {…}
      }
      
    3. 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 {…}
      }