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?
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.