如何在Notification Service Extension中检测SwiftUI应用是否处于后台状态?
这个问题我之前也碰到过,核心原因是Notification Service Extension作为独立的扩展进程,和主App不在同一个运行空间里,所以根本没法直接访问主App的UIApplication实例——这就是为什么UIApplication.shared会报错的原因。下面给你几个可行的解决方案,按推荐优先级排序:
方案一:让前台推送直接跳过Extension调用(最可靠)
其实默认情况下,如果主App在前台时处理了推送且选择不显示通知,Notification Service Extension是不会被触发的。你可以检查下主App的推送代理实现是否正确:
步骤1:设置推送代理(SwiftUI App生命周期)
import SwiftUI import UserNotifications class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // 设置推送中心的代理为当前AppDelegate UNUserNotificationCenter.current().delegate = self return true } // 前台收到推送时的处理方法 func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { // 选择不显示通知,这样Extension就不会被调用 completionHandler(.none) // 如果你需要在前台处理推送逻辑,在这里写就行,不用走Extension PushNotificationHelper.handleNotification(userInfo: notification.request.content.userInfo, context: persistentContainer.viewContext) } } @main struct YourApp: App { // 适配UIApplicationDelegate @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var body: some Scene { WindowGroup { ContentView() } } }
这样设置后,只要主App在前台,推送会直接走到willPresent方法,Extension完全不会被触发,完美符合你的需求。
方案二:通过Payload标记区分前台/后台推送
如果你的业务需求必须让Extension在前台时也被调用,但需要跳过处理逻辑,可以让服务器在推送时根据主App的状态(比如主App可以主动上报自己的状态给服务器)添加一个标记字段,比如isForeground:
Extension中的处理代码
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) guard let bestAttemptContent = bestAttemptContent else { contentHandler(request.content) return } // 读取推送Payload中的前台标记 let userInfo = request.content.userInfo if let isAppForeground = userInfo["isForeground"] as? Bool, isAppForeground { // 应用处于前台,直接返回原通知内容,不执行处理逻辑 contentHandler(bestAttemptContent) return } // 后台状态下执行你的处理逻辑 PushNotificationHelper.handleNotification(userInfo: userInfo, context: persistentContainer.viewContext) contentHandler(bestAttemptContent) }
这种方法的好处是不需要额外的状态同步,完全依赖推送Payload的标记,准确性很高,但需要服务器配合。
方案三:通过App Groups共享应用状态
如果不想依赖服务器,可以利用iOS的App Groups机制,让主App把自己的运行状态同步到共享存储空间,Extension再读取这个状态:
步骤1:配置App Groups
在Xcode中,分别给主App和Notification Service Extension添加同一个App Group(比如group.com.yourcompany.yourapp),具体操作是:
- 选中你的项目 -> 选择主App Target -> 切换到
Signing & Capabilities标签 -> 点击+ Capability-> 添加App Groups-> 新建或选择已有的Group - 重复上述操作给Extension Target添加同一个App Group
步骤2:主App同步状态到共享存储
import SwiftUI @main struct YourApp: App { @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { ContentView() } .onChange(of: scenePhase) { newPhase in // 获取共享的UserDefaults guard let sharedDefaults = UserDefaults(suiteName: "group.com.yourcompany.yourapp") else { return } // 根据场景状态更新标记 switch newPhase { case .active: sharedDefaults.set(true, forKey: "isAppForeground") case .inactive, .background: sharedDefaults.set(false, forKey: "isAppForeground") @unknown default: sharedDefaults.set(false, forKey: "isAppForeground") } sharedDefaults.synchronize() } } }
步骤3:Extension读取共享状态
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) guard let bestAttemptContent = bestAttemptContent else { contentHandler(request.content) return } // 读取共享存储中的应用状态 guard let sharedDefaults = UserDefaults(suiteName: "group.com.yourcompany.yourapp") else { contentHandler(bestAttemptContent) return } let isAppForeground = sharedDefaults.bool(forKey: "isAppForeground") if isAppForeground { // 前台状态直接返回,不处理 contentHandler(bestAttemptContent) return } // 后台状态执行处理逻辑 PushNotificationHelper.handleNotification(userInfo: request.content.userInfo, context: persistentContainer.viewContext) contentHandler(bestAttemptContent) }
这种方法的缺点是状态同步可能有轻微延迟,但大部分场景下足够使用。
内容的提问来源于stack exchange,提问作者alionthego

