You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在App后台/关闭状态下保存FCM远程推送通知?

FCM推送后台/关闭状态下无法保存到CoreData和Firestore的问题

我的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 21:44:50