What's on your mind?
iOS Communication Notifications
A practical guide for adding WhatsApp-style iOS notifications to a React Native app that already uses OneSignal.
When a chat or group message arrives, iOS can show the sender or group avatar instead of only the app icon. That UI is an Apple Communication Notification. OneSignal still delivers the push. The Notification Service Extension converts the payload into an incoming INSendMessageIntent.
Backend
→ OneSignal
→ APNs
→ Notification Service Extension
→ Read custom data
→ Create incoming INSendMessageIntent
→ Download avatar
→ notificationContent.updating(from: intent)
→ Communication Notification UI
This is not a normal notification attachment. Attaching an image to UNNotificationContent.attachments does not create Communication Notification UI. You must use updating(from:).
Test on a real iPhone. Simulator is not reliable for remote push or Communication Notification presentation.
1. Requirements
- iOS 15+
- An existing iOS Notification Service Extension
- Push Notifications capability on the main app
- Communication Notifications capability on the main app
mutable-content: 1 in the APNs payload (OneSignal sets this when an NSE exists)
- Avatar URLs in the push custom data
You do not need:
- A second Notification Service Extension
- A new bundle ID
- An App Group just for this feature
- Replacing OneSignal with native APNs
An App Group is only needed if the app and extension must share storage. Communication Notifications themselves do not require one.
Apple allows one Notification Service Extension per app. If two NSEs are embedded, iOS may not run the one you modified.
2. Inspect the existing project first
Do not assume payload keys, target names, or entitlements. Find the real ones.
Check:
- Main app target and bundle ID
- Notification Service Extension target and bundle ID
NotificationService.swift
- Existing OneSignal NSE integration
- Existing
INSendMessageIntent usage (Siri / Share Sheet)
- Main app
Info.plist → NSUserActivityTypes
- Main app entitlements
- NSE entitlements
- App Groups, if any
- OneSignal
additionalData shape used by the JS/native click handler
If Siri Suggestions or Share Sheet already donate INSendMessageIntent, keep that outgoing flow. Incoming notifications use the same intent type with interaction.direction = .incoming.
3. Xcode configuration
Main app target
Enable:
- Push Notifications
- Communication Notifications
- Siri, only if the app already uses Siri / Share Sheet suggestions
Confirm the main app entitlements contain:
<key>com.apple.developer.usernotifications.communication</key>
<true/>
Confirm Info.plist includes:
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
Do not duplicate INSendMessageIntent if it is already there.
Notification Service Extension
Use the existing NSE. Typical bundle ID:
com.yourapp.bundleid.OneSignalNotificationServiceExtension
Link Intents.framework to the NSE target.
Do not add Communication Notifications capability again on the NSE unless Apple’s signing setup specifically requires it. The containing app entitlement is the one that matters.
Keep mutable-content enabled. OneSignal does this automatically when a Notification Service Extension is present.
4. Payload
Read OneSignal custom data from the NSE userInfo dictionary. OneSignal stores additional data here:
Also handle fallbacks:
userInfo["os_data"]["custom"]["a"]
userInfo["additionalData"]
- top-level keys, if
additional_data_is_root_payload is enabled
Do not invent keys. Use the keys your backend already sends.
Recommended additional data
1-to-1
{
"type": "single_chat",
"id": "chat_123",
"sender": {
"id": "user_456",
"name": "Nicole Fuentes",
"profile": "https://cdn.example.com/nicole.jpg"
}
}
Group
{
"type": "group_chat",
"group": {
"id": "group_123",
"title": "Family",
"image": "https://cdn.example.com/family.jpg"
},
"sender": {
"id": "user_456",
"name": "Nicole Fuentes",
"profile": "https://cdn.example.com/nicole.jpg"
}
}
The APNs alert title and body can still be used as fallbacks for name and message text.
Fields that matter
| Purpose |
Typical keys |
| Chat vs group vs other |
type |
| 1:1 conversation id |
id, chat_id |
| Group conversation id |
group.id, group_id |
| Group name |
group.title, group.name |
| Group avatar |
group.image |
| Sender id |
sender.id, sender_id |
| Sender name |
sender.name |
| Sender avatar |
sender.profile, sender.image |
If group.image is missing, iOS can still show Communication Notification layout, but it cannot show that group’s own avatar.
Apply Communication Notifications only to chat types. Leave invitations, missed calls, marketing, and other pushes unchanged.
5. Notification Service Extension flow
Keep OneSignal processing first. Then apply Communication Notification handling. Never replace OneSignal.
Pattern:
OneSignalExtension.didReceiveNotificationExtensionRequest(
receivedRequest,
with: bestAttemptContent,
withContentHandler: { onesignalContent in
CommunicationNotificationProcessor.process(content: onesignalContent) { processed in
contentHandler(processed)
}
}
)
Call contentHandler only once. If avatar download is still running when the extension is about to expire, deliver the best content you already have.
override func serviceExtensionTimeWillExpire() {
OneSignalExtension.serviceExtensionTimeWillExpireRequest(receivedRequest, with: bestAttemptContent)
contentHandler(bestAttemptContent)
}
Never drop the notification because an image failed.
6. Create the incoming intent
Shared pieces
- Parse custom data.
- Decide 1:1 vs group vs ignore.
- Download avatars with a short timeout.
- Create
INPerson for the sender.
- Create
INSendMessageIntent.
- Set
INInteraction.direction = .incoming.
- Update content with
try content.updating(from: intent).
1-to-1
sender = the other person
speakableGroupName = nil
recipients = nil
conversationIdentifier = chat id
- avatar comes from the sender
INPerson image
let sender = INPerson(
personHandle: INPersonHandle(value: senderId, type: .unknown),
nameComponents: nil,
displayName: senderName,
image: senderImage,
contactIdentifier: nil,
customIdentifier: senderId,
isMe: false,
suggestionType: .none
)
let intent = INSendMessageIntent(
recipients: nil,
outgoingMessageType: .outgoingMessageText,
content: message,
speakableGroupName: nil,
conversationIdentifier: conversationId,
serviceName: "YourApp",
sender: sender,
attachments: nil
)
Group
speakableGroupName = group name
conversationIdentifier = group id
sender = the person who wrote the message
- group avatar is set on
speakableGroupName, not as a normal notification attachment
iOS is more reliable about showing the group avatar when the intent has at least two recipients.
intent.setImage(groupImage, forParameterNamed: \.speakableGroupName)
If the group image download fails, fall back to the sender image, then to no image. Still show the notification.
Incoming direction
let interaction = INInteraction(intent: intent, response: nil)
interaction.direction = .incoming
interaction.donate(completion: nil)
let updated = try content.updating(from: intent)
contentHandler(updated)
updating(from:) is what turns the banner into a Communication Notification.
If the app already donates outgoing INSendMessageIntent for Siri / Share Sheet, reuse the same conversationIdentifier values. Do not rewrite the outgoing donation path unless it is broken.
7. Avatar download
Download inside the NSE with URLSession. Do not add a third-party image library unless the extension already has one that is safe to use there.
Rules:
- Use a short timeout (about 8–10 seconds). NSE execution time is limited.
- Accept only
http / https URLs.
- Handle invalid URLs, HTTP failures, and decode failures.
- Resize the image (around 256px) before creating
INImage.
- Cache in the extension’s own Caches directory. No App Group required.
- If download fails, continue without an avatar.
let image = INImage(imageData: jpegData)
8. What not to do
- Do not create another Notification Service Extension.
- Do not create another bundle ID.
- Do not replace OneSignal.
- Do not treat a notification attachment as a Communication Notification.
- Do not apply this UI to every push type.
- Do not block forever waiting for an image.
- Do not silently drop a notification on timeout.
- Do not duplicate Communication Notifications capability if it is already enabled.
- Do not create an App Group unless shared storage is actually required.
- Do not break existing Siri Suggestions / Share Sheet intent donations.
9. Testing
Use a physical iPhone.
- Build and run the main app on device so the NSE is installed with it.
- Put the app in the background or lock the phone.
- Send a 1:1 chat push with
sender.profile.
- Confirm the banner shows the sender name and sender avatar.
- Send a group chat push with
group.image and sender.profile.
- Confirm the banner shows the group name and group avatar.
- Send a non-chat push and confirm it still looks like a normal notification.
- Confirm Siri / Share Sheet chat suggestions still work, if the app has them.
If the NSE is not running, Communication Notification UI will never appear. Confirm mutable-content is set and that only one NSE is embedded.
10. Troubleshooting
| Symptom |
Likely cause |
| Still seeing only the app icon |
updating(from:) was not used, or NSE did not run |
| NSE never runs |
Missing mutable-content, or a second NSE is embedded |
| Group name shows, but app icon remains |
group.image missing, download failed, or setImage was not applied to speakableGroupName |
| Chat notifications work, other pushes broke |
Communication handling was applied to every notification type |
| Notification never appears |
contentHandler was not called on timeout or error |
| Share Sheet / Siri suggestions broke |
Outgoing INSendMessageIntent donation was changed |
| Works in code, fails in Simulator |
Expected. Test on a real device |
11. This repository
This project implements the feature in the existing OneSignal NSE:
| File |
Role |
ios/OneSignalNotificationServiceExtension/NotificationService.swift |
OneSignal first, then Communication Notification processing, timeout-safe delivery |
ios/OneSignalNotificationServiceExtension/CommunicationNotificationProcessor.swift |
Payload parsing, INPerson, incoming INSendMessageIntent, updating(from:) |
ios/OneSignalNotificationServiceExtension/AvatarImageDownloader.swift |
Timed avatar download, decode, resize, local cache |
Existing config reused:
- Main app bundle ID:
com.networkmessenger.network
- NSE bundle ID:
com.networkmessenger.network.OneSignalNotificationServiceExtension
- Communication Notifications entitlement already on the main app
INSendMessageIntent already in NSUserActivityTypes
- Outgoing Siri / Share Sheet donations left unchanged
Conversation IDs match existing donations:
- 1:1 → chat
id
- Group →
group.id
- Event chat →
event.id
Chat types handled:
single_chat
single_chat_reaction
group_chat
group_chat_reaction
event_chat
event_chat_reaction
12. Apple references
Code of Conduct
What's on your mind?
iOS Communication Notifications
A practical guide for adding WhatsApp-style iOS notifications to a React Native app that already uses OneSignal.
When a chat or group message arrives, iOS can show the sender or group avatar instead of only the app icon. That UI is an Apple Communication Notification. OneSignal still delivers the push. The Notification Service Extension converts the payload into an incoming
INSendMessageIntent.This is not a normal notification attachment. Attaching an image to
UNNotificationContent.attachmentsdoes not create Communication Notification UI. You must useupdating(from:).Test on a real iPhone. Simulator is not reliable for remote push or Communication Notification presentation.
1. Requirements
mutable-content: 1in the APNs payload (OneSignal sets this when an NSE exists)You do not need:
An App Group is only needed if the app and extension must share storage. Communication Notifications themselves do not require one.
Apple allows one Notification Service Extension per app. If two NSEs are embedded, iOS may not run the one you modified.
2. Inspect the existing project first
Do not assume payload keys, target names, or entitlements. Find the real ones.
Check:
NotificationService.swiftINSendMessageIntentusage (Siri / Share Sheet)Info.plist→NSUserActivityTypesadditionalDatashape used by the JS/native click handlerIf Siri Suggestions or Share Sheet already donate
INSendMessageIntent, keep that outgoing flow. Incoming notifications use the same intent type withinteraction.direction = .incoming.3. Xcode configuration
Main app target
Enable:
Confirm the main app entitlements contain:
Confirm
Info.plistincludes:Do not duplicate
INSendMessageIntentif it is already there.Notification Service Extension
Use the existing NSE. Typical bundle ID:
Link Intents.framework to the NSE target.
Do not add Communication Notifications capability again on the NSE unless Apple’s signing setup specifically requires it. The containing app entitlement is the one that matters.
Keep
mutable-contentenabled. OneSignal does this automatically when a Notification Service Extension is present.4. Payload
Read OneSignal custom data from the NSE
userInfodictionary. OneSignal stores additional data here:Also handle fallbacks:
userInfo["os_data"]["custom"]["a"]userInfo["additionalData"]additional_data_is_root_payloadis enabledDo not invent keys. Use the keys your backend already sends.
Recommended additional data
1-to-1
{ "type": "single_chat", "id": "chat_123", "sender": { "id": "user_456", "name": "Nicole Fuentes", "profile": "https://cdn.example.com/nicole.jpg" } }Group
{ "type": "group_chat", "group": { "id": "group_123", "title": "Family", "image": "https://cdn.example.com/family.jpg" }, "sender": { "id": "user_456", "name": "Nicole Fuentes", "profile": "https://cdn.example.com/nicole.jpg" } }The APNs alert
titleandbodycan still be used as fallbacks for name and message text.Fields that matter
typeid,chat_idgroup.id,group_idgroup.title,group.namegroup.imagesender.id,sender_idsender.namesender.profile,sender.imageIf
group.imageis missing, iOS can still show Communication Notification layout, but it cannot show that group’s own avatar.Apply Communication Notifications only to chat types. Leave invitations, missed calls, marketing, and other pushes unchanged.
5. Notification Service Extension flow
Keep OneSignal processing first. Then apply Communication Notification handling. Never replace OneSignal.
Pattern:
Call
contentHandleronly once. If avatar download is still running when the extension is about to expire, deliver the best content you already have.Never drop the notification because an image failed.
6. Create the incoming intent
Shared pieces
INPersonfor the sender.INSendMessageIntent.INInteraction.direction = .incoming.try content.updating(from: intent).1-to-1
sender= the other personspeakableGroupName=nilrecipients=nilconversationIdentifier= chat idINPersonimageGroup
speakableGroupName= group nameconversationIdentifier= group idsender= the person who wrote the messagespeakableGroupName, not as a normal notification attachmentiOS is more reliable about showing the group avatar when the intent has at least two recipients.
If the group image download fails, fall back to the sender image, then to no image. Still show the notification.
Incoming direction
updating(from:)is what turns the banner into a Communication Notification.If the app already donates outgoing
INSendMessageIntentfor Siri / Share Sheet, reuse the sameconversationIdentifiervalues. Do not rewrite the outgoing donation path unless it is broken.7. Avatar download
Download inside the NSE with
URLSession. Do not add a third-party image library unless the extension already has one that is safe to use there.Rules:
http/httpsURLs.INImage.8. What not to do
9. Testing
Use a physical iPhone.
sender.profile.group.imageandsender.profile.If the NSE is not running, Communication Notification UI will never appear. Confirm
mutable-contentis set and that only one NSE is embedded.10. Troubleshooting
updating(from:)was not used, or NSE did not runmutable-content, or a second NSE is embeddedgroup.imagemissing, download failed, orsetImagewas not applied tospeakableGroupNamecontentHandlerwas not called on timeout or errorINSendMessageIntentdonation was changed11. This repository
This project implements the feature in the existing OneSignal NSE:
ios/OneSignalNotificationServiceExtension/NotificationService.swiftios/OneSignalNotificationServiceExtension/CommunicationNotificationProcessor.swiftINPerson, incomingINSendMessageIntent,updating(from:)ios/OneSignalNotificationServiceExtension/AvatarImageDownloader.swiftExisting config reused:
com.networkmessenger.networkcom.networkmessenger.network.OneSignalNotificationServiceExtensionINSendMessageIntentalready inNSUserActivityTypesConversation IDs match existing donations:
idgroup.idevent.idChat types handled:
single_chatsingle_chat_reactiongroup_chatgroup_chat_reactionevent_chatevent_chat_reaction12. Apple references
UNNotificationContent.updating(from:)Code of Conduct