iOS: Rich Notifications

As of iOS 10, Apple gave developers the ability to produce Rich Notifications. These are capable of displaying attachments (thumbnails), or displaying media such as images, animated GIFs and videos. You can also add adding a custom UI to your rich notifications. You can send and activate Rich Notifications using Sailthru Mobile.

Because your app can receive Rich Notifications alongside regular push notifications, you need to complete a few steps to tell your app how to determine when to display attachments:

  1. Add a Notification Service Extension to enable Rich Notifications
  2. Implement the Extension code and write the logic to download and display the attachment
  3. Send Messages with Pushes attached with Sailthru Mobile dashboard

Add a Notification Service Extension

iOS 10 brings two new app extensions for push notifications: a Notification Service Extension and a Notification Content Extensions.

To display basic attachments such as images or animated GIFs, you will only need a Notification Services Extension. This extension is activated as the notification arrives but before it is presented to the user. You have about 30 seconds to modify the push notification content such as text or attachments and then present it to the user.

To start, add a Notification Service Extension Target to your application by choosing File, New, Target:

732

Choosing Notification Service Extension to add as a new Target.

Enable Push Notifications

If you have not done so already, you need to enable Push Notifications as a capability and set up provisioning, which is similar to setting up basic iOS push.

1166

Turning on Push in the Capabilities screen of your target.

Implement the Extension Code

You need to write code so that your Service Extension can download and handle the attachment you want to display.

In this example, we want to attach a GIF to our Rich Notification. To do so, we will send the URL to the image using a payload attribute we call image_url. Our Service Extension is already configured to accept videos too. If you wish, you can pass a valid video stream to the video_url payload attribute. If you specify both, our Service Extension will display the video.

Notice how we're not writing code to size and position our image, as iOS takes care of that automatically. Your code will only need to take care of downloading the resource and saving it to a temporary location.

Inside the Service Extension, modify your code to look like the below code block. This will download a resource (image, video, GIF) that you include in the push payload. It will then write it to the internal storage and attach it to the push notification.

πŸ“˜

Note

If you are using the Extensions Framework you can use the STMNotificationServiceExtension class to handle the default implementation for you! More details here.

#import "NotificationService.h"

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    // Modify the notification content here...
    [self marigoldRichNotificationAttachments:self.bestAttemptContent withResponse:^(UNMutableNotificationContent * _Nullable content) {
        self.bestAttemptContent = content;
        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.downloadTask cancel];
    
    self.contentHandler(self.bestAttemptContent);
}

- (void)marigoldRichNotificationAttachments:(UNMutableNotificationContent *)originalContent withResponse:(nullable void(^)(UNMutableNotificationContent *__nullable modifiedContent))block  {
    // For Image or Video in-app messages, we will send the media URL in the
    // _st payload
    NSString *imageURL = originalContent.userInfo[@"_st"][@"image_url"];
    NSString *videoURL = originalContent.userInfo[@"_st"][@"video_url"];
    
    NSURL *attachmentURL = nil;
    if (videoURL && ![videoURL isKindOfClass:[NSNull class]]) { //Prioritize videos over image
        attachmentURL = [NSURL URLWithString:videoURL];
    }
    else if (imageURL && ![imageURL isKindOfClass:[NSNull class]]) {
        attachmentURL = [NSURL URLWithString:imageURL];
    }
    else {
        block(originalContent); //Nothing to add to the push, return early.
        return;
    }
    
    NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration]];
    self.downloadTask = [session downloadTaskWithURL:attachmentURL completionHandler:^(NSURL *fileLocation, NSURLResponse *response, NSError *error) {
        if (error != nil) {
            block(originalContent); //Nothing to add to the push, return early.
            return;
        }
        else {
            NSFileManager *fileManager = [NSFileManager defaultManager];
            NSString *fileSuffix = attachmentURL.lastPathComponent;
            
            NSURL *typedAttachmentURL = [NSURL fileURLWithPath:[(NSString *_Nonnull)fileLocation.path stringByAppendingString:fileSuffix]];
            [fileManager moveItemAtURL:fileLocation toURL:typedAttachmentURL error:&error];
            
            NSError *attachError = nil;
            UNNotificationAttachment *attachment = [UNNotificationAttachment attachmentWithIdentifier:@"" URL:typedAttachmentURL options:nil error:&attachError];
            
            if (attachment == nil) {
                block(originalContent); //Nothing to add to the push, return early.
                return;
            }
            
            UNMutableNotificationContent *modifiedContent = originalContent.mutableCopy;
            [modifiedContent setAttachments:[NSArray arrayWithObject:attachment]];
            block(modifiedContent);
        }
    }];
    [self.downloadTask resume];
}

@end
import UserNotifications

class NotificationService: UNNotificationServiceExtension {
    
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    var downloadTask: URLSessionDownloadTask?
    
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        if let bestAttemptContent = bestAttemptContent {
            if let title = bestAttemptContent.userInfo["title"] {
                bestAttemptContent.title = title as! String
            }
            
            var urlString:String?
            
            // Prioritize video over image
            if let stPayload = bestAttemptContent.userInfo["_st"] as? Dictionary<AnyHashable, Any> {
                if let videoURL = stPayload["video_url"] {
                    urlString = videoURL as? String
                } else if let imageURL = stPayload["image_url"] {
                    urlString = imageURL as? String
                } else {
                    // Nothing to add to the push, return early.
                    contentHandler(bestAttemptContent)
                    return
                }
            }
            
            sailthruHandleAttachmentDownload(content: bestAttemptContent.userInfo, urlString: urlString!)
    
        }
    }
    
    func sailthruHandleAttachmentDownload(content: [AnyHashable : Any], urlString: String) {
        
        guard let url = URL(string: urlString) else {
            // Cannot create a valid URL, return early.
            self.contentHandler!(self.bestAttemptContent!)
            return
        }
        
        self.downloadTask = URLSession.shared.downloadTask(with: url) { (location, response, error) in
            if let location = location {
                let tmpDirectory = NSTemporaryDirectory()
                let tmpFile = "file://".appending(tmpDirectory).appending(url.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!)
        }
        
        self.downloadTask?.resume()
    }
    
    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.
        self.downloadTask?.cancel()
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
}

Send Rich Notifications

When creating an in-app message with push notification via the Sailthru Mobile Web Interface, the image added to the in-app message will automatically be delivered to the iOS device inside the UNMutableNotificationContent _st payload. If using the extension code above, this will then be displayed as a Rich Notification on the iOS device.

1105

Creating a message with an image inside the Sailthru Mobile platform.

Alternatively, rich notifications can be sent using the API as per the Rich Push example.

Setting Category Identifiers

πŸ“˜

Categories are optional and do not need to be included. If you do, however, the push notification service extension will require that category to activate.

You can set a Category Identifier for your Notification to respond to. This identifier contains a string value you determine, and it will be used by the Service Extension to determine how it should respond to this notification category (or if it should respond at all). You will use this identifier to register for push notifications and include it in your payload when sending.

You can add one or many categories in the plist of the extension. While normally you will only require one value, multiple identifiers are useful with Content Extensions, when you may want to perform different tasks depending on the UI your app will present to the user.

To add a Category Identifier, navigate to your Service Extension's Info.plist and add UNNotificationExtensionCategory (String) under NSExtension -> NSExtensionAttributes:

708

A single category identifier registered.

If you need to specify multiple values, change to UNNotificationExtensionCategory to be an Array:

596

Multiple category identifiers registered. Change the UNNotificationExtensionCategory type to Array from String to add multiple values