Notification Service Extension is an iOS extension that intercepts a push notification before it is displayed to the user and allows modifying the content. The extension can decrypt encrypted data, download media attachments, change text, or add custom fields. According to Apple Developer, 2025, Notification Service Extension has up to 30 seconds to complete the task in the background.
Key Takeaways
Notification Service Extension is an app extension in iOS that runs before a push notification is displayed. It allows the server to send a minimal payload while the extension enriches it with content: downloading images, decrypting data, replacing text. The user sees the already processed notification.
When iOS receives a push notification, the system checks if an extension exists for the app. If the extension is registered, iOS launches it in a background process and passes the notification content via the didReceive(_:withContentHandler:) method. The extension processes the content and calls contentHandler with the modified content. If the extension does not complete within 30 seconds, the system displays the original notification.
Service Extension is needed in scenarios where notification content requires on-device processing. Secure communications: the server sends an encrypted payload, the extension decrypts it locally. Rich media notifications: the server sends an image URL, the extension downloads and attaches it. Dynamic localization: the extension substitutes text in the device language.
| Scenario | Without Extension | With Extension |
|---|---|---|
| Image | Not supported | Downloaded and displayed |
| Encryption | Server stores the key | Decryption on device |
| Text | Hardcoded on the server | Dynamic substitution |
| Validation | Not checked | Malicious notifications cancelled |
Notification Service Extension runs in a limited environment. Maximum execution time is 30 seconds. The extension does not have access to the main app storage (except App Group). The extension size is limited to 50 MB. When time exceeds, the system calls contentHandler with the original content, and all changes are lost.
In Xcode, Notification Service Extension is added by creating a new target of type Notification Service Extension. Xcode generates a template NotificationService class inheriting from UNNotificationServiceExtension, with two methods: didReceive and serviceExtensionTimeWillExpire.
In Xcode, select File → New → Target → Notification Service Extension. Enter a name (e.g., PushNotificationService) and choose Swift language. Make sure the target is added to the main app and has the correct signing. After creation, the NotificationService.swift class with a basic implementation will be generated.
The extension Info.plist contains the NSExtension key with subkeys NSExtensionPointIdentifier (com.apple.usernotifications.service) and NSExtensionPrincipalClass (your controller name). Additionally, NSExtensionAttributes can be specified with extension activation rules. Xcode generates these settings automatically.
The basic implementation overrides didReceive, modifies the notification content, and calls contentHandler. If processing takes too long, serviceExtensionTimeWillExpire is called, where you need to complete work with the current state.
import UserNotifications
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
request: UNNotificationRequest,
withContentHandler handler: @escaping (UNNotificationContent) -> Void
) {
contentHandler = handler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
bestAttemptContent.title = "[Processed] \(bestAttemptContent.title)"
contentHandler?(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
if let contentHandler = contentHandler,
let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
One of the key uses of Service Extension is decrypting notification content on the device. The server sends an encrypted payload, and the extension decrypts it using a key stored in Keychain or App Group. This ensures the content is not interceptable during transmission.
The server encrypts the notification body using a symmetric key (AES-256). The encryption key is pre-agreed between the client and server. The extension receives the encrypted string in the data field of the payload, decrypts it, and substitutes it into the notification fields. The user key can be obtained from Keychain after authentication.
override func didReceive(
request: UNNotificationRequest,
withContentHandler handler: @escaping (UNNotificationContent) -> Void
) {
guard let content = request.content.mutableCopy()
as? UNMutableNotificationContent else {
handler(request.content)
return
}
guard let encryptedData = content.userInfo["encrypted_data"]
as? String else {
handler(content)
return
}
let decrypted = CryptoService.decrypt(encryptedData)
content.body = decrypted.body
content.title = decrypted.title
handler(content)
}
The decryption key must not be stored in the extension code or NSUserDefaults. Use iOS Keychain with access via App Group so that both the app and the extension can read the key. Use Security.framework with AES-256-GCM algorithm for key generation on the client.
The most common use of Notification Service Extension is downloading images, GIFs, and videos for display in the notification. The server sends a media file URL, and the extension downloads it, saves it to a temporary directory, and creates a UNNotificationAttachment.
The extension receives the image URL from the notification payload field. Using URLSession, the extension downloads the file to a temporary directory. After download completes, a UNNotificationAttachment with the local URL is created. The attachment is passed to the modified content. iOS automatically displays the image in the standard interface or in a Notification Content Extension.
private func downloadAndAttachMedia(
content: UNMutableNotificationContent,
mediaUrl: String,
handler: @escaping (UNNotificationContent) -> Void
) {
guard let url = URL(string: mediaUrl) else {
handler(content)
return
}
let task = URLSession.shared.downloadTask(with: url) { localUrl, _, error in
guard let localUrl = localUrl, error == nil else {
handler(content)
return
}
let attachment = try? UNNotificationAttachment(
identifier: "media",
url: localUrl,
options: nil
)
if let attachment = attachment {
content.attachments = [attachment]
}
handler(content)
}
task.resume()
}
iOS supports the following formats for notification display: JPEG, PNG, GIF (static), MPEG-4 video up to 50 MB. For audio files, MP3, AAC, and ALAC are supported. Important: all media files must be downloaded within the 30-second limit. For large files, server-side cropping or progressive loading is recommended.
Managing the 30-second limit is a key task when developing Notification Service Extension. If the extension does not complete processing in time, the system calls serviceExtensionTimeWillExpire and displays the original content. Fallback scenarios must be provided for each type of processing.
Divide tasks by priority. Perform critical modifications (decryption, basic localization) first. Optional improvements (image downloading, text enrichment) come second. Use URLSession with timeouts for network requests to avoid spending the entire limit on a single operation.
If image download fails or payload decryption returns an error, the extension should call contentHandler with the original content. Never terminate the extension without calling contentHandler — this leads to notification loss. A safe fallback should always be implemented in the extension code.
override func didReceive(
request: UNNotificationRequest,
withContentHandler handler: @escaping (UNNotificationContent) -> Void
) {
let content = (request.content.mutableCopy()
as? UNMutableNotificationContent) ?? request.content
// Critical task: decryption
var decryptedContent = tryDecryptPayload(content)
// Optional task: media
guard let mediaUrl = decryptedContent.userInfo["media_url"]
as? String else {
handler(decryptedContent)
return
}
downloadAndAttachMedia(
content: decryptedContent,
mediaUrl: mediaUrl,
handler: handler
)
}
To test Notification Service Extension, use Xcode: select the extension target, run on the simulator, and send a push notification via terminal or Firebase Console. Log each processing stage using os_log — this helps diagnose timing and download issues.
Frequently Asked Questions
The system calls serviceExtensionTimeWillExpire, then displays the original notification unchanged. All downloaded files and modifications are discarded.
Yes, if you call contentHandler with empty content (UNNotificationContent with empty fields), the notification will not be displayed. This is used for filtering spam notifications or invalid data.
The token can be passed in the userInfo of the notification payload or retrieved from Keychain via App Group. It is not recommended to store tokens in the extension UserDefaults.
The extension must be signed with the same developer certificate as the main app. For production, a production certificate with the Push Notifications capability enabled is required.
Connect the device to Xcode, select the extension target in the run scheme, and send a push notification via Firebase Console. Breakpoints in the extension work the same as in the main app.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also