480480

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 Carnival.

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 Carnival 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:

732732

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.

11661166

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.

#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 carnivalRichNotificationAttachements: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)carnivalRichNotificationAttachements:(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"];

    // You can also specify a media URL by defining your own payload keys
    NSString *imageURL = originalContent.userInfo[@"my_image_url"]; 
    NSString *videoURL = originalContent.userInfo[@"my_video_url"];

    NSURL *attachmentURL = nil;
    if (![videoURL isKindOfClass:[NSNull class]]) { //Prioritize videos over image
        attachmentURL = [NSURL URLWithString:videoURL];
    }
    else if (![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 videoURL = bestAttemptContent.userInfo["video_url"] {
                urlString = videoURL as? String
            } else if let imageURL = bestAttemptContent.userInfo["image_url"] {
                urlString = imageURL as? String
            } else {
                // Nothing to add to the push, return early.
                contentHandler(bestAttemptContent)
                return
            }
            
            carnivalHandleAttachmentDownload(content: bestAttemptContent.userInfo, urlString: urlString!)
    
        }
    }
    
    func carnivalHandleAttachmentDownload(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

The most common way you'll want to send a Rich Notification is via our API. In the notification payload, you define a Rich Notification by setting the mutable_content flag to true and by specifying one of the category identifiers you defined earlier (it must match one of the values you defined in your Service Extension's Info.plist). The Service Extension will also look for the image URL using the image_url or video_url keys. Make sure you specify these values.

Here is an example:

curl -X POST -u :API_KEY -H "Content-type: application/json" -H 'Accept: application/json' https://api.carnivalmobile.com/v5/notifications -d '{
  "notification": {
    "to": [{ "name": "custom.boolean.forecast_user", "criteria": [true]}],
    "payload": {
      "alert": "Cloudy today. The high will be 55°. Tap to read the forecast for your area.",
      "title": "Your weather forecast",
      "badge": 1,
      "sound": "Default.caf",
      "mutable_content": true,
        "image_url": "https://example.com/cloudy.gif"
    }
  }
}'

You can also send notifications via Dashboard. In the Category field, make sure you use one category identifiers you defined in your Service Extension's Info.plist:

589589

Use Key-Value data to fill in the correct fields and set a category.

If you create a Video or Image in-app message with a push attached, you will find the URL to the media content inside the UNMutableNotificationContent's _st payload.

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:

708708

A single category identifier registered.

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

596596

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

curl -X POST -u :API_KEY -H "Content-type: application/json" -H 'Accept: application/json' https://api.carnivalmobile.com/v4/notifications -d '{
  "notification": {
    "to": [{ "name": "tags", "criteria": ["tag1"]}, { "name": "custom.string.mykey", "criteria": ["tag2"]}],
    "payload": {
      "alert": "Congratulations Rose, you earned Gold Status. Tap here to discover all the benefits.",
      "title": "You just reached Gold Status!",
      "badge": 1,
      "sound": "Default.caf",
      "category": "LOYALTY_MESSAGE",
      "mutable_content": true,
        "video_url": "https://example.com/gold.mp4"
    }
  }
}'