如何在App后台/关闭状态下保存FCM远程推送通知?
我的App可以将FCM远程推送通知保存到CoreData和Firestore数据库,但仅在App前台运行或用户与通知交互时生效。当App处于后台或关闭状态,用户直接打开App而不点击通知时,推送内容无法被保存。
我了解到可通过在payload中设置"content-available":1实现后台通知处理,但FCM控制台无法修改默认payload。不确定是AppDelegate缺少对应处理方法,还是需搭建服务端发送自定义payload的通知。以下是完整的AppDelegate代码:
import SwiftUI import FirebaseCore import UserNotifications import UIKit import Firebase import FirebaseFirestore import FirebaseMessaging import FirebaseAnalytics import CoreData @main struct PGUApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate let persistenceController = PersistenceController.shared var body: some Scene { WindowGroup { ContentView().environment(\.managedObjectContext, persistenceController.persistentContainer.viewContext) } } } class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, MessagingDelegate { let gcmMessageIDKey = "gcm.message_id" func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { print("Application didFinishLaunchingWithOptions called") FirebaseApp.configure() let center = UNUserNotificationCenter.current() center.requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in if let error = error { print("Authorization request error: \(error)") } else { print("Authorization granted: \(granted)") if granted { DispatchQueue.main.async { application.registerForRemoteNotifications() } } else { print("User denied notification permissions") // Handle the denial of permissions here } } } center.delegate = self Messaging.messaging().delegate = self return true } func applicationDidBecomeActive(_ application: UIApplication) { } func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { Messaging.messaging().apnsToken = deviceToken print("APNs device token retrieved: \(deviceToken)") } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { print("Failed to register for remote notifications: \(error.localizedDescription)") } func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { print("FCM Token received: \(fcmToken ?? "nil")") if let token = fcmToken { saveTokenToFirestore(token) } } func saveTokenToFirestore(_ token: String) { // Assuming you have a `userId` to associate with the token let userId = "someUserId" // Replace with actual user ID let db = Firestore.firestore() db.collection("user_tokens").document(userId).setData(["fcmToken": token]) { error in if let error = error { print("Error saving token to Firestore: \(error)") } else { print("Token successfully saved to Firestore") } } } func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let userInfo = notification.request.content.userInfo if let notificationDict = userInfo["aps"] as? [String: AnyObject], let alertDict = notificationDict["alert"] as? [String: AnyObject], let title = alertDict["title"] as? String, let body = alertDict["body"] as? String { saveNotificationToCoreData(title: title, body: body) } saveNotificationToFirestore(userInfo) completionHandler([.alert, .sound]) } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let userInfo = response.notification.request.content.userInfo if let notificationDict = userInfo["aps"] as? [String: AnyObject], let alertDict = notificationDict["alert"] as? [String: AnyObject], let title = alertDict["title"] as? String, let body = alertDict["body"] as? String { saveNotificationToCoreData(title: title, body: body) } saveNotificationToFirestore(userInfo) completionHandler() } func saveNotificationToFirestore(_ userInfo: [AnyHashable: Any]) { guard let aps = userInfo["aps"] as? [String: AnyObject], let alert = aps["alert"] as? [String: String], let title = alert["title"], let body = alert["body"] else { print("Invalid notification content") return } let db = Firestore.firestore() let notificationData: [String: Any] = [ "title": title, "body": body, "timestamp": FieldValue.serverTimestamp(), ] db.collection("notifications").addDocument(data: notificationData) { error in if let error = error { print("Error saving notification to Firestore: \(error)") } else { print("Notification successfully saved to Firestore") } } } func saveNotificationToCoreData(title: String, body: String) { let context = PersistenceController.shared.persistentContainer.viewContext let newNotification = Notifi(context: context) newNotification.title = title newNotification.body = body do { try context.save() } catch { print("Failed to save notification: \(error)") } printAllNotifications() } func printAllNotifications() { let context = PersistenceController.shared.persistentContainer.viewContext let fetchRequest: NSFetchRequest<Notifi> = Notifi.fetchRequest() do { let notifications = try context.fetch(fetchRequest) for notification in notifications { print("Title: \(notification.title ?? "Unknown Title"), Body: \(notification.body ?? "No Body")") } } catch let error as NSError { print("Could not fetch. \(error), \(error.userInfo)") } } }
1. 补全AppDelegate的后台推送处理逻辑
你当前的application(_:didReceiveRemoteNotification:fetchCompletionHandler:)方法是空的,这是App在后台状态下接收推送并处理的核心入口,需要补充保存逻辑:
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { // 解析推送内容并保存到CoreData if let notificationDict = userInfo["aps"] as? [String: AnyObject], let alertDict = notificationDict["alert"] as? [String: AnyObject], let title = alertDict["title"] as? String, let body = alertDict["body"] as? String { saveNotificationToCoreData(title: title, body: body) } // 保存到Firestore saveNotificationToFirestore(userInfo) // 必须调用completionHandler,告知系统处理完成状态 completionHandler(.newData) }
同时,需要确保App开启了后台推送权限:
- 在Xcode中进入项目的
Signing & Capabilities页面,添加Background Modes权限,勾选Remote notifications; - 或者直接在Info.plist中添加
UIBackgroundModes数组,包含remote-notification项。
2. 处理App关闭状态下的推送
当App完全关闭时,带有content-available:1的推送会唤醒App并触发didFinishLaunchingWithOptions,需要在这里也加入推送处理逻辑:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { print("Application didFinishLaunchingWithOptions called") FirebaseApp.configure() // ... 原有授权代码保持不变 ... // 处理App关闭时收到的推送 if let remoteNotification = launchOptions?[.remoteNotification] as? [AnyHashable: Any] { if let notificationDict = remoteNotification["aps"] as? [String: AnyObject], let alertDict = notificationDict["alert"] as? [String: AnyObject], let title = alertDict["title"] as? String, let body = alertDict["body"] as? String { saveNotificationToCoreData(title: title, body: body) } saveNotificationToFirestore(remoteNotification) } return true }
3. 自定义Payload的发送方式
FCM控制台无法直接设置content-available:1,你需要通过以下方式发送自定义payload:
方式一:使用FCM API发送
通过HTTP请求发送包含content_available: true的推送,示例请求体(POST到https://fcm.googleapis.com/fcm/send):
{ "to": "目标设备的FCM Token", "content_available": true, "notification": { "title": "推送标题", "body": "推送内容" }, "data": { "自定义字段": "自定义值" } }
请求头需要包含Authorization: key=你的服务器密钥(在Firebase控制台的项目设置->云消息传递中获取)。
方式二:使用Firebase Cloud Functions
编写云函数,在需要触发推送时构造包含content_available: true的payload,示例代码:
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); exports.sendCustomNotification = functions.https.onRequest(async (req, res) => { const payload = { content_available: true, notification: { title: "测试推送", body: "后台可处理的推送内容" } }; await admin.messaging().sendToDevice("目标FCM Token", payload); res.send("推送发送成功"); });
内容的提问来源于stack exchange,提问作者Chamoy

