Overview
Rich Push Notifications in iOS allow you to include media content such as images, GIFs, videos, and audio files, along with interactive buttons, in your notifications. This feature enhances the user experience by providing visually appealing and actionable notifications.
Use Case
Purpose:
Provide rich media notifications to captivate users' attention.
Include actionable buttons to drive user interactions without opening the app.
Benefits:
Improve notification engagement with rich, interactive content.
Enable immediate actions with buttons embedded in notifications.
Implementation Details
Create a Notification Service Extension:
Add a new target for the Notification Service Extension in Xcode:
Go to File > New > Target > Notification Service Extension.
Name the extension (e.g.,
NotificationService).
Configure the Notification Service: Implement the
didReceiveNotificationRequestmethod inNotificationService.swiftorNotificationService.mto handle rich content. The extension reads the media URL from theios_apx_mediakey in the push payload.var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent UNUserNotificationCenter.current().getNotificationCategories { categories in if let categoryIdentifier = self.bestAttemptContent?.categoryIdentifier, let lc = request.content.userInfo["aps"] as? [String: Any], let lcCode = lc["lc"] as? String { self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_" + lcCode let categoryExists = categories.contains { $0.identifier == self.bestAttemptContent?.categoryIdentifier } if !categoryExists { self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_en" } } if let urlString = request.content.userInfo["ios_apx_media"] as? String, let fileUrl = URL(string: urlString) { URLSession.shared.downloadTask(with: fileUrl) { location, response, error in if let location = location { let tmpDirectory = NSTemporaryDirectory() let tmpFile = "file://".appending(tmpDirectory).appending(fileUrl.lastPathComponent) let tmpUrl = URL(string: tmpFile)! try? FileManager.default.moveItem(at: location, to: tmpUrl) if let attachment = try? UNNotificationAttachment(identifier: "", url: tmpUrl) { self.bestAttemptContent?.attachments = [attachment] } } self.contentHandler!(self.bestAttemptContent!) }.resume() } else { self.contentHandler!(self.bestAttemptContent!) } } } override func serviceExtensionTimeWillExpire() { if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent { contentHandler(bestAttemptContent) } }- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler { self.contentHandler = contentHandler; self.bestAttemptContent = [request.content mutableCopy]; [[UNUserNotificationCenter currentNotificationCenter] getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> * _Nonnull categories) { NSString *categoryIdentifier = self.bestAttemptContent.categoryIdentifier; NSDictionary *lc = request.content.userInfo[@"aps"]; if (categoryIdentifier && lc) { self.bestAttemptContent.categoryIdentifier = [NSString stringWithFormat:@"%@_%@", categoryIdentifier, lc[@"lc"]]; } NSMutableSet *set = [[categories objectsPassingTest:^BOOL(UNNotificationCategory * _Nonnull obj, BOOL * _Nonnull stop) { return [obj.identifier isEqualToString:self.bestAttemptContent.categoryIdentifier]; }] mutableCopy]; if ([set count] == 0) { self.bestAttemptContent.categoryIdentifier = [NSString stringWithFormat:@"%@_%@", categoryIdentifier, @"en"]; } }]; NSString *urlString = request.content.userInfo[@"ios_apx_media"]; [self loadAttachmentForUrlString:urlString completionHandler:^(UNNotificationAttachment *attachment) { if (attachment) { self.bestAttemptContent.attachments = [NSArray arrayWithObject:attachment]; } self.contentHandler(self.bestAttemptContent); }]; } - (void)serviceExtensionTimeWillExpire { self.contentHandler(self.bestAttemptContent); } - (void)loadAttachmentForUrlString:(NSString *)urlString completionHandler:(void(^)(UNNotificationAttachment *))completionHandler { __block UNNotificationAttachment *attachment = nil; NSURL *attachmentURL = [NSURL URLWithString:urlString]; [[[NSURLSession sharedSession] downloadTaskWithURL:attachmentURL completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error != nil) { NSLog(@"%@", error.localizedDescription); } else { NSString *dir = NSTemporaryDirectory(); NSString *tempFile = [NSString stringWithFormat:@"%@%@%@", @"file://", dir, [attachmentURL lastPathComponent]]; NSURL *tmpUrl = [NSURL URLWithString:tempFile]; NSFileManager *fileManager = [NSFileManager defaultManager]; [fileManager moveItemAtURL:location toURL:tmpUrl error:&error]; NSError *attachmentError = nil; attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:tmpUrl options:nil error:&attachmentError]; if (attachmentError) { NSLog(@"%@", attachmentError.localizedDescription); } } completionHandler(attachment); }] resume]; }Include Media in Notifications:
Add the media URL to the notification payload under the
ios_apx_mediakey. This is the same key the Notification Service Extension reads in the snippets above; payloads withoutios_apx_media(for example usingmedia-urlormutable-contentalone) will not produce an attachment.Ensure the media file is accessible and properly formatted.
Interactive Buttons:
Add actions to your notification category and handle button clicks using
UNUserNotificationCenterDelegatemethods.
Localized Buttons Behavior
If you are using localized buttons in your push notifications on iOS, it's important to note the following behavior:
Localized buttons may not appear consistently if the push is sent as a regular (data-only) push.
The buttons appear reliably only when the push is sent as a rich push, i.e., with an image or other media included.
Why this happens: On iOS, localization for buttons is handled in the Notification Service Extension, which is only triggered for rich push notifications. If the extension isn't activated (as with a regular push), the localized buttons won't be applied correctly.
The category-localization block from the snippets above, shown standalone for reference:
UNUserNotificationCenter.current().getNotificationCategories { categories in
if let categoryIdentifier = self.bestAttemptContent?.categoryIdentifier,
let lc = request.content.userInfo["aps"] as? [String: Any],
let lcCode = lc["lc"] as? String {
self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_" + lcCode
let categoryExists = categories.contains { $0.identifier == self.bestAttemptContent?.categoryIdentifier }
if !categoryExists {
self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_en"
}
}
}To ensure localized buttons work as expected, always send the notification as a rich push, even if the media isn't essential for the message.
Keep in mind
Rich push notifications require iOS 10 or later.
Test media downloads and rendering across various devices and network conditions.
Use lightweight media files to ensure a smooth user experience.