如何在FCM中使用apns-collapse-id实现iOS通知分组?
iOS FCM通知合并/分组问题解决方案
我之前也踩过这个FCM在iOS上配置通知合并的坑,尤其是用Cloud Functions发送的时候,apns-collapse-id的配置位置确实容易搞混,而且官方文档的示例有时候不够明确。结合你的代码和问题,我来帮你梳理一下正确的做法:
首先明确需求:替换旧通知 vs 分组折叠通知
- 如果是用新通知替换相同类型的旧通知(不堆叠,只显示最新的一条),需要用
apns-collapse-id(对应APNs的collapse-id) - 如果是将相似通知分组折叠(可以展开查看所有同组通知),需要用
thread-id(APNs payload里的thread-id)
先针对你要的合并/分组场景,给出正确的代码配置:
正确配置方式:直接控制APNs Payload
你的问题出在:当使用FCM的notification字段时,FCM会自动生成APNs的payload,可能会覆盖或忽略你设置的apns字段配置。更可靠的方式是直接在apns.payload.aps里定义通知内容和合并规则,同时用data字段传递业务参数。
示例1:替换旧通知(用collapse-id)
const payload = { // 业务数据放在data里,客户端Flutter点击后处理 data: { click_action: "FLUTTER_NOTIFICATION_CLICK", status: "done", sound: "default" }, // 直接配置APNs的payload,确保合并规则生效 apns: { payload: { aps: { alert: { title: "Your medical history is updated" + Math.random(), // 如果有副标题或内容也可以加在这里 // body: "具体更新内容" }, sound: "default", // 关键:设置collapseId,对应APNs的collapse-id,相同ID的通知会被替换 collapseId: "MedicalHistory" } }, // 也可以在headers里设置apns-collapse-id,和payload里的collapseId二选一即可 // headers: { // "apns-collapse-id": "MedicalHistory" // }, headers: { "apns-priority": "10" // 高优先级,确保通知及时送达 } } }; // 不需要额外设置collapseKey(collapseKey是Android用的,iOS不认) const options = { priority: "high" }; await admin.messaging().sendToDevice("MY_TOKEN", payload, options);
示例2:分组折叠通知(用thread-id)
如果是想让同类型的通知折叠在一起(比如多条医疗记录更新通知可以展开查看),把上面的collapseId换成thread-id即可:
aps: { alert: { title: "Your medical history is updated" + Math.random(), }, sound: "default", threadId: "MedicalHistory" // 相同threadId的通知会被分组折叠 }
为什么你的之前代码无效?
- FCM notification字段的干扰:当你设置
notification字段时,FCM会自动生成APNs的alert内容,可能会忽略你在apns.payload.aps里的配置,导致collapse-id没有被正确传递到APNs。 - collapseKey不生效:你设置的
collapseKey是Android平台的合并规则,iOS完全不识别这个参数,iOS只认APNs的collapse-id或thread-id。 - 配置层级错误:
apns-collapse-id需要放在apns.headers或者apns.payload.aps.collapseId里,你的代码层级是对的,但因为notification字段的存在,FCM可能没有正确转发这个配置。
额外注意事项
collapse-id/thread-id是大小写敏感的,必须完全一致才能触发合并/分组。- 确保iOS客户端的通知权限已经正确获取(包括通知显示权限)。
- 如果用Flutter处理通知,确保你在
firebase_messaging的配置中正确处理了APNs的参数,比如在onMessage或onBackgroundMessage中解析通知内容。
内容的提问来源于stack exchange,提问作者Neat




