Rich Push Notifications

Prev Next

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

  1. Purpose:

    • Provide rich media notifications to captivate users' attention.

    • Include actionable buttons to drive user interactions without opening the app.

  2. Benefits:

    • Improve notification engagement with rich, interactive content.

    • Enable immediate actions with buttons embedded in notifications.


Implementation Details

  1. 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).

  2. Configure the Notification Service: Implement the didReceiveNotificationRequest method in NotificationService.m or NotificationService.swift to handle rich content:

    For Objective-C:

    - (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
           self.contentHandler = contentHandler;
           self.bestAttemptContent = [request.content mutableCopy];
           //categories
          [[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 {
       // Called just before the extension will be terminated by the system.
       // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
       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];
    }

    For Swift:

    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"] {
                    self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_" + ((lc as! NSDictionary)["lc"] as! String)
                    let categoryExistArray = categories.filter { (category) -> Bool in
                        category.identifier == self.bestAttemptContent?.categoryIdentifier
                    }
                    if categoryExistArray.isEmpty {
                        self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_en"
                    }
                }
                if let urlString = request.content.userInfo["ios_apx_media"], let fileUrl = URL(string: urlString as? String ?? "") {
                    // Download the attachment
                    URLSession.shared.downloadTask(with: fileUrl ) { (location, response, error) in
                        if let location = location {
                            // Move temporary file to remove .tmp extension
                            let tmpDirectory = NSTemporaryDirectory()
                            let tmpFile = "file://".appending(tmpDirectory).appending(fileUrl.lastPathComponent)
                            let tmpUrl = URL(string: tmpFile)!
                            try! FileManager.default.moveItem(at: location, to: tmpUrl)
                            // Add the attachment to the notification content
                            if let attachment = try? UNNotificationAttachment(identifier: "", url: tmpUrl) {
                                self.bestAttemptContent?.attachments = [attachment]
                            }
                        }
                        // Serve the notification content
                        self.contentHandler!(self.bestAttemptContent!)
                        }.resume()
                } else {
                    self.contentHandler!(self.bestAttemptContent!)
                }
            }
        }
        override func serviceExtensionTimeWillExpire() {
            // Called just before the extension will be terminated by the system.
            // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
            if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
                contentHandler(bestAttemptContent)
            }
        }

  3. Include Media in Notifications:

    • Add the media URL to the notification payload under the media-url key.

    • Ensure the media file is accessible and properly formatted.

  4. Interactive Buttons:

    • Add actions to your notification category and handle button clicks using UNUserNotificationCenterDelegate methods.


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.

UNUserNotificationCenter.current().getNotificationCategories { (categories) in
            if let categoryIdentifier = self.bestAttemptContent?.categoryIdentifier, let lc = request.content.userInfo["aps"] {
                self.bestAttemptContent?.categoryIdentifier = categoryIdentifier + "_" + ((lc as! NSDictionary)["lc"] as! String)
                let categoryExistArray = categories.filter { (category) -> Bool in
                    category.identifier == self.bestAttemptContent?.categoryIdentifier
                }
                if categoryExistArray.isEmpty {
                    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.