Search code examples
iosswiftasynchronous

Swift concurrency: 'for await' does not compile, 'await' has warning


I have this code, which works and runs with Xcode 15.4 and does not produce a warning. It is in general what is told to be valid, in this post.

 private func waitForUnlockWithTimeout() async throws -> Bool {
    let awaitUnlockNotificationTask = Task {
      let notification = await NotificationCenter.default.notifications(named: UIApplication.protectedDataDidBecomeAvailableNotification)
      print("keychain is available now")
      return true
    }

    let timeoutTask = Task {
      try await Task.sleep(nanoseconds: 1_000_000_000)
      print("keychain unlock timeout - waited for one second and still locked.")
      awaitUnlockNotificationTask.cancel()
    }

    return await withTaskCancellationHandler {
      let result = await awaitUnlockNotificationTask.value
      timeoutTask.cancel()
      return result
    } onCancel: {
      awaitUnlockNotificationTask.cancel()
      timeoutTask.cancel()
    }
  }

This code produces a warning in Xcode 16 beta: No 'async' operations occur within 'await' expression

After reading the docs, I came to the conclusion, that this is kind of an event stream and I can listen to it with for await.

I changed the code of the first task into this:

    let awaitUnlockNotificationTask = Task {
      for await notification in NotificationCenter.default.notifications(named: UIApplication.protectedDataDidBecomeAvailableNotification) {
        print("keychain is available now")
        return true
      }
      return false
    }

This works on Xcode 16 beta, and the warning is gone as well. However, it does not compile with Xcode 15.4 anymore. It says

Expression is 'async' but is not marked with 'await'

Apple docs are also quite interesting. It is not async in the function header, but the docs state:

notifications(named:object:) Returns an asynchronous sequence of notifications produced by this center for a given notification name and optional source object.

What am I missing?


Solution

  • I do not believe this is doing what you intended it to:

    let awaitUnlockNotificationTask = Task {
        let notification = await NotificationCenter.default.notifications(named: UIApplication.protectedDataDidBecomeAvailableNotification)
        print("keychain is available now")
        return true
    }
    

    The variable notification is not the notification, but rather the asynchronous sequence of (future) notifications. And you do not have to await the fetching of the sequence unless it is isolated to a separate concurrency context.

    The deeper issue is that awaitUnlockNotificationTask will return as soon as the sequence is created, but not actually await any notifications, itself. So this task will return true after the AsyncSequence is created (but undoubtedly before any notifications are actually received).

    Perhaps you intended to await the first notification in that sequence with prefix (or first). If you do that, it will now await the first notification received on that asynchronous sequence of notifications:

    private func waitForUnlockWithTimeout() async -> Bool {
        let awaitUnlockNotificationTask = Task { @MainActor in
            let sequence = NotificationCenter.default
                .notifications(named: UIApplication.userDidTakeScreenshotNotification)
                .map { _ in true }
                .prefix(1)
    
            for await _ in sequence { }
    
            return !Task.isCancelled
        }
    
        let timeoutTask = Task {
            try await Task.sleep(for: .seconds(10))
            awaitUnlockNotificationTask.cancel()
        }
    
        return await withTaskCancellationHandler {
            let result = await awaitUnlockNotificationTask.value
            timeoutTask.cancel()
            return result
        } onCancel: {
            awaitUnlockNotificationTask.cancel()
        }
    }
    

    Note, I've changed that to a non-throwing function as you do not try anything.

    Or, if you want to throw an error on timeout, you could do something like the following, where no Bool return value is needed anymore:

    private func waitForUnlockWithTimeout() async throws {
        let awaitUnlockNotificationTask = Task { @MainActor in
            let sequence = NotificationCenter.default
                .notifications(named: UIApplication.userDidTakeScreenshotNotification)
                .map { _ in true }
                .prefix(1)
    
            for await _ in sequence { }
    
            try Task.checkCancellation()
        }
    
        let timeoutTask = Task {
            try await Task.sleep(for: .seconds(10))
            awaitUnlockNotificationTask.cancel()
        }
    
        return try await withTaskCancellationHandler {
            try await awaitUnlockNotificationTask.value
            timeoutTask.cancel()
        } onCancel: {
            awaitUnlockNotificationTask.cancel()
        }
    }
    

    There are lots of possible variations on the theme, so do not get lost in the details. The main point is that notifications returns an asynchronous sequence (which, itself, does not inherently require an await), but you probably want to await the first value generated by that sequence.