Use await for NotificationCenter.default.notifications throws warning in Xcode 16, but not in Xcode 15

With Xcode 15.1, this code

func test1() async {
    let _ = await NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification)
    return
}

would compile fine, but in Xcode 16, the same code will give warning "No async operations occur within await expression".

The function signature is

public func notifications(named name: Notification.Name, object: AnyObject? = nil) -> NotificationCenter.Notifications

which isn't a async function. I just wonder why in Xcode 15 it needs await while in Xcode 16 it doesn't?

Maybe it has something to do with changes with actor? I noticed that if I mark test1 with @MainActor, both Xcode 15 and 16 don't require me to use await. Is it because NotificationCenter.default.notifications now runs on MainActor by default?

1 Like

The reason is didBecomeActiveNotification is bounded to UIApplication which is MainActor isolated class.
Declaration is different from swift 6 and before it

this is swift 6 declaration

extension UIApplication {
    nonisolated public class let willEnterForegroundNotification: NSNotification.Name

    nonisolated public class let didFinishLaunchingNotification: NSNotification.Name

    nonisolated public class let didBecomeActiveNotification: NSNotification.Name

    nonisolated public class let willResignActiveNotification: NSNotification.Name

    nonisolated public class let didReceiveMemoryWarningNotification: NSNotification.Name
}

and this is the original declaration

extension UIApplication {
    public class let willEnterForegroundNotification: NSNotification.Name

    public class let didFinishLaunchingNotification: NSNotification.Name

    public class let didBecomeActiveNotification: NSNotification.Name

    public class let willResignActiveNotification: NSNotification.Name

    public class let didReceiveMemoryWarningNotification: NSNotification.Name
}

So before swift 6 these global variable was accidentally bounded to MainActor. And these is the reason why await kicks in.

4 Likes

And how do you get rid of the warning? The docs say to use compact or map to get the Sendable payload, but this didBecomeActiveNotification doesn't have userInfo.

.map { _ in } if you don’t care about the value

1 Like

I forgot to update this. That's what we're doing in our codebase.

    let values = notificationCenter
      .notifications(named: UIApplication.didBecomeActiveNotification)
      .map { _ in }

Thanks anyway.

NotificationCenter's documentation includes a discussion and solution to this issue.

Notification doesn’t conform to Sendable, because several of its members can’t be sendable, such as object and the userInfo dictionary. If you iterate over the notifications in this sequence, Xcode issues a warning about crossing an actor boundary. To handle this, use a map(_:) or compactMap(_:) to extract sendable properties of the notification, such as its name or sendable values from the userInfo dictionary.

The following macOS example calls notifications(named:object:) to receive an asynchronous sequence of didActivateApplicationNotification notifications from the shared NSWorkspace notification center. It then uses a compactMap(_:) to retrieve NSRunningApplication values from the notification’s userInfo dictionary, from which it gets localizedName strings. Since the String type conforms to Sendable , you can iterate over this type without a compiler warning.

for await appName in NSWorkspace.shared.notificationCenter.notifications (
    named: NSWorkspace.didActivateApplicationNotification)
    .compactMap ({ notification in
        guard let appInfo = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication,
              let name = appInfo.localizedName else { return nil as String? }
        return name
    })
{
    print("Application activated: \(appName)")
}