iOS push notifications

iOS push setup, categories, badge counts, alerts, and testing

Leanplum is serving iOS push notifications by natively using Apple Push Notification Service (APNS).

To enable Push Notifications on iOS, you need to upload your certificates to Leanplum and register for remote notifications in your app. For more on set up and customization of iOS push in Leanplum:

❗️

We use swizzling to collect push tokens. If your app has other SDKs installed that also use method swizzling it may cause conflicts. See here for manual instructions.

iOS Setup

Below is more information setting up pushes on iOS

STEP 1.

Login to the your Apple Developer iOS provisioning portal.

STEP 2.

In the Identifiers > App IDs section, select your app, click Edit, and enable Push Notifications.

STEP 3.

Click Create Certificate for each of the Development and Production certificates and follow the onscreen instructions. You should not reuse existing certificates so that we can track delivery failures properly. For Sandbox generate a Sandbox certificate, for Production a Production/Sandbox combined certificate. Apple does not offer the option of Production only certificates anymore.

STEP 4.

Download your new certificate files from your browser. Open the files on your computer, which will launch Keychain.

STEP 5.

In Keychain, select the new certificates, expand them to view the private key, and then right click to export them as .p12 files. You must enter a password.

STEP 6.

In Leanplum, go to your app's Keys & Settings (App Settings > {Your app} > Keys & Settings). Under Push Notifications, upload your .p12 files to Leanplum and enter your passphrase from step 5 above.

STEP 7.

Configure your app to use push notifications in your app delegate's applicationDidFinishLaunching method. Example:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

    //iOS-10
    if #available(iOS 10.0, *){
        let userNotifCenter = UNUserNotificationCenter.current()

        userNotifCenter.requestAuthorization(options: [.badge,.alert,.sound]){ (granted,error) in
            //Handle individual parts of the granting here.

            if granted {
                DispatchQueue.main.async {
                    UIApplication.shared.registerForRemoteNotifications()
                }
            }
        }
    }
    //iOS 8-9
    else if #available(iOS 8.0, *){
        let settings = UIUserNotificationSettings.init(types: [UIUserNotificationType.alert,UIUserNotificationType.badge,UIUserNotificationType.sound],
                                        categories: nil)
        UIApplication.shared.registerUserNotificationSettings(settings)
        UIApplication.shared.registerForRemoteNotifications()
    }
    //iOS 7
    else{
        UIApplication.shared.registerForRemoteNotifications(matching:
            [UIRemoteNotificationType.alert,
             UIRemoteNotificationType.badge,
             UIRemoteNotificationType.sound])
    }
    //Other code.
}
//Note: You can choose any combination of formats — this is just an example.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    id notificationCenterClass = NSClassFromString(@"UNUserNotificationCenter");
    if (notificationCenterClass) {
        // iOS 10.
        SEL selector = NSSelectorFromString(@"currentNotificationCenter");
        id notificationCenter =
        ((id (*)(id, SEL)) [notificationCenterClass methodForSelector:selector])
        (notificationCenterClass, selector);
        if (notificationCenter) {
            selector = NSSelectorFromString(@"requestAuthorizationWithOptions:completionHandler:");
            IMP method = [notificationCenter methodForSelector:selector];
            void (*func)(id, SEL, unsigned long long, void (^)(BOOL, NSError *__nullable)) =
            (void *) method;
            func(notificationCenter, selector,
                 0b111, /* badges, sounds, alerts */
                 ^(BOOL granted, NSError *__nullable error) {
                     if (granted) {
                        [[UIApplication sharedApplication] registerForRemoteNotifications];
                     }
                     if (error) {
                         NSLog(@"Leanplum: Failed to request authorization for user "
                               "notifications: %@", error);
                     }
                 });
        }
    } else if ([[UIApplication sharedApplication] respondsToSelector:
                @selector(registerUserNotificationSettings:)]) {
        // iOS 8-9.
        UIUserNotificationSettings *settings = [UIUserNotificationSettings
                                                settingsForTypes:UIUserNotificationTypeAlert |
                                                UIUserNotificationTypeBadge |
                                                UIUserNotificationTypeSound categories:nil];
        [[UIApplication sharedApplication] registerUserNotificationSettings:settings];
        [[UIApplication sharedApplication] registerForRemoteNotifications];
    } else {
        // iOS 7 and below.
        #pragma clang diagnostic push
        #pragma clang diagnostic ignored "-Wdeprecated-declarations"
        [[UIApplication sharedApplication] registerForRemoteNotificationTypes:
        #pragma clang diagnostic pop
         UIUserNotificationTypeSound | UIUserNotificationTypeAlert | UIUserNotificationTypeBadge];
    }

    // Other code.
}
//Note: You can choose any combination of formats — this is just an example.

Note that registerForRemoteNotifications method must be called when authorization is successful. If notifications are registered but authorization not granted, the user might get a push token, but will not be push enabled since the user has not enabled notifications - notifications are disabled.

📘

Note that you don't have to add this if you plan on using the Push Pre-Permission message, which will bring up the iOS prompt for you. You can also choose to bring up the prompt later in the app.

If you are using UNUserNotification.framework, make sure you implement UNUserNotificationCenterDelegate and its methods, to be able to receive events and actions when user opens the app from status bar, or when push notification arrives when app is in foreground.

import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
  
      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        return true
    }
  
      func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    }
}
@interface AppDelegate ()

@end

@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 
{
    [UNUserNotificationCenter currentNotificationCenter].delegate = self;
    return YES;
}

-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{

}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{

}

@end

🚧

If you are implementing application:didReceiveRemoteNotification:fetchCompletionHandler in your code, you should call the completion handler yourself.

iOS Push Payload

Below is a sample push notification payload sent from Leanplum through APNS to an iOS device:

[AnyHashable("_lpm"): 529231658988123, AnyHashable("lp_occurrence_id"): 7c5e7ddd-a1db-41c0-8aa5-7377f5ea123, AnyHashable("_lpx"): {
    URL = "http://www.leanplum.com";
    "__name__" = "Open URL";
}, AnyHashable("aps"): {
    alert = TestMessage;
    "content-available" = 1;
    "mutable-content" = 1;
}, AnyHashable("apns-push-type"): alert, AnyHashable("LP_URL"): https://assets.prod.leanplum.com/app_ve9UCNlqI8dy6Omzf.../bW96YXJ0LmpwZw==]

iOS Customizations

Below is more information on customizing your push set up

Push categories

Starting with iOS 8, Apple added support to create categories for your push notifications. This allows you to provide more interactivity to your notifications with custom actions.

In lieu of this change, Leanplum supports the ability for you to categorize of your push notifications, track each custom action, and even define the logic for what happens on each action in the dashboard.

For the ability to use categories with your push notifications in Leanplum, there is additional implementation that is required, which will handle custom actions in your app delegate. If you call [Leanplum handleActionWithIdentifier] method already, you should not call completionHandler also because Leanplum calls this internally when it's ready.

NOTE: It's a good practice to show additional information or to deeplink to the part of your app that matches your intended action for that user when they open your push notification. You can do that using Leanplum's in-app messaging, which integrates nicely with push notifications. There's no additional development needed in that scenario.

//Create category
let generalCategory = UNNotificationCategory(identifier: "GENERAL",
                                             actions: [],
                                             intentIdentifiers: [],
                                             options: .customDismissAction)
 
//Register category.
let center = UNUserNotificationCenter.current()
center.setNotificationCategories([generalCategory])
//Register the notification category
UNNotificationCategory* generalCategory = [UNNotificationCategory
     categoryWithIdentifier:@"GENERAL"
     actions:@[]
     intentIdentifiers:@[]
     options:UNNotificationCategoryOptionCustomDismissAction];
 
// Register the notification categories.
UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
[center setNotificationCategories:[NSSet setWithObjects:generalCategory, nil]];

Custom Category and Actions Set up

The Push Notification's Category is defined in your apps code and set in the Leanplum dashboard. This is found in your Push message under iOS options. Actions are defined in the Dashboard and must be handled in the App code as well.

As you can see in the image below, once set on the dashboard, your Category names and custom actions are easily selectable for each message in Leanplum.

316316

In the steps below, you will find more information on how to set up custom categories and use custom actions on iOS.

STEP 1.

Define the Category and Actions inside the App Delegate's didFinishLaunching method:

let dismiss = UNNotificationAction(identifier: "DismissAction", title: "Dismiss", options: .destructive)
let snooze = UNNotificationAction(identifier: "SnoozeAction", title: "Snooze", options: .foreground)
let enter = UNNotificationAction(identifier: "EnterAction", title: "Enter", options: .foreground)
let category = UNNotificationCategory(identifier: "CustomCategory", actions: [snooze, enter, dismiss], intentIdentifiers: [""], options: [.customDismissAction])
UNUserNotificationCenter.current().setNotificationCategories([category])
UNNotificationAction* enter = [UNNotificationAction
        actionWithIdentifier:@"EnterAction"
        title:@"Enter"
        options:UNNotificationActionOptionForeground];
 UNNotificationAction* snooze = [UNNotificationAction
        actionWithIdentifier:@"SnoozeAction"
        title:@"Snooze"
        options:UNNotificationActionOptionForeground];
 UNNotificationAction* dismiss = [UNNotificationAction
        actionWithIdentifier:@"DismissAction"
        title:@"Dismiss"
        options:UNNotificationActionOptionDestructive];
 UNNotificationCategory* category = [UNNotificationCategory
             categoryWithIdentifier:@"CustomCategory"
             actions:@[snooze, enter, dismiss]
             intentIdentifiers:@[@""]
             options:UNNotificationCategoryOptionCustomDismissAction];
         
 UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter];
        [center setNotificationCategories:[NSSet setWithObjects:category, nil]];

STEP 2.

Set the Notification Center Delegate. If you have already done that, continue to the next step.

let userNotifCenter = UNUserNotificationCenter.current()
userNotifCenter.delegate = self
[UNUserNotificationCenter currentNotificationCenter].delegate = self;

STEP 3.

Configure the actions code and handle the user's response:

@available(iOS 10.0, *)
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
     
    let userInfo = response.notification.request.content.userInfo
// Check if any of the custom actions is executed
        if response.actionIdentifier == "SnoozeAction" || response.actionIdentifier == "EnterAction"
        || response.actionIdentifier == "DismissAction" {
            if let mid = userInfo["_lpn"] as? Int64 { // get the message id
                // Use the below code to track only the message event without executing an action
                // You can also use context?.runActionNamed(action)
                //let ct = Leanplum.createActionContext(forMessageId: String(mid))
                //let action = response.actionIdentifier.replacingOccurrences(of: "Action", with: "")
                //ct?.trackMessageEvent(action, withValue: 0, andInfo: "", andParameters: nil)
                 
                var args = [AnyHashable:Any]()
                let action = response.actionIdentifier.replacingOccurrences(of: "Action", with: "")
                if let actions = userInfo[LP_KEY_PUSH_CUSTOM_ACTIONS] as? [AnyHashable:Any] { // access the custom actions _lpc
                    args[action] = actions[action]
                }
                let context = LPActionContext.init(name: LP_PUSH_NOTIFICATION_ACTION, args: args, messageId: String(mid)) // Init the context with the arguments, messageId and the reserved name - __Push Notification
                context?.runTrackedActionNamed(action) // Execute the Action and track the message event
            }
        }
         
        completionHandler()
    }
-(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
    NSDictionary *userInfo = [[[[response valueForKey:@"notification"] valueForKey:@"request"] valueForKey:@"content"] valueForKey:@"userInfo"];

    if([[response actionIdentifier] isEqual:@"SnoozeAction"]||[[response actionIdentifier] isEqual:@"EnterAction"]||[[response actionIdentifier] isEqual:@"DismissAction"]){
        int64_t i = [[userInfo objectForKey:@"_lpn"] longLongValue];
        NSMutableDictionary * args = [[NSMutableDictionary alloc] init];
        NSString* action = [[response actionIdentifier] stringByReplacingOccurrencesOfString:@"Action" withString:@""];
        NSDictionary* actions = [userInfo objectForKey:@"LP_KEY_PUSH_CUSTOM_ACTIONS"];
        if(actions != NULL){
            [args setObject:[actions objectForKey:action] forKey:action];
        }
        LPActionContext *context = [LPActionContext actionContextWithName:LP_PUSH_NOTIFICATION_ACTION args:args messageId:[NSString stringWithFormat:@"%lld",i]];
        [context runTrackedActionNamed:action];
    }
}

Keep in mind that you need to set the Category to be used in the Push Notifications in the Dashboard. This can be seen in the above image.

This is how the above custom actions look on the end users device:

375375

📘

A Few Notes on Implementing Custom Actions

  • Custom actions are triggered by long-pressing the push notification.
  • If your custom action 'Opens' the app, the custom action could be overridden by the main Open Action of the Push Notification.

Badge counts

Similarly to setting custom push message categories and actions, you are able to customize your app badge count with Leanplum.

The app icon badge is meant to display a count of unread items that await the user's attention. By design, this relies entirely on a remote server to be the system of record. While each push notification automatically increments this value, you can set the count with a badge attribute in the payload to override this. If the value is 0, iOS will clear the count and remove the badge. If a value is not set in the payload, the count remains the same as the remote server.

For example, in the below payload, we set the badge count to 9.

{
    "aps" : {
        "alert" : "You got your emails.",
        "badge" : 9
    },
    "acme1" : "bar",
    "acme2" : 42
}

Similarly, in this payload, you can send a badge count of 0. This will clear the badge count, even if the user does not open the notification. A silent push works the same way, but would not include an alert value.

{
    "aps" : {
        "alert" : "You got your emails.",
        "badge": 0
    },
    "acme1" : "bar",
    "acme2" : 42
}

In the Leanplum message and campaign composer, you can set the badge value in your push notification to any number you choose. By default, leaving the field blank, will lead to default behavior and maintain the current badge count. However, like above if you set this field to zero, you will clear the count on the users iPhone.

You cannot increment the existing badge count directly from Leanplum's dashboard, but you can add some code to your app to increment and clear the badge count.

To increment the badge whenever a push is received in the background, add the following to didReceiveRemoteNotification in your delegate:

func application(_ application: UIApplication,
                          didReceiveRemoteNotification userInfo: [AnyHashable : Any],
                          fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

  // Only increment badge if in background.
  if application.applicationState == .background {
    UIApplication.shared.applicationIconBadgeNumber += 1
  }

  completionHandler(.newData)
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {

  // Only increment badge if in background.
  if(application.applicationState == UIApplicationStateBackground) {
    NSInteger badge = [[UIApplication sharedApplication] applicationIconBadgeNumber];
    [[UIApplication sharedApplication] setApplicationIconBadgeNumber: badge + 1];
  }

  completionHandler(UIBackgroundFetchResultNewData);
}

To clear the count on app start or resume, you could implement the following in your applicationDidBecomeActive delegate method. However, this would clear the count even if they open the app by some means other than opening the push notification.

func applicationDidBecomeActive(_ application: UIApplication) {
    // Clear app badge on start or resume.
    UIApplication.shared.applicationIconBadgeNumber = 0
}
func applicationDidBecomeActive(_ application: UIApplication) {
    // Clear app badge on start or resume.
    UIApplication.shared.applicationIconBadgeNumber = 0
}func applicationDidBecomeActive(_ application: UIApplication) {
    // Clear app badge on start or resume.
    UIApplication.shared.applicationIconBadgeNumber = 0
}

Custom alert sounds

You can set a custom alert sound for a push notification via the Leanplum Message Composer. However, before doing so, you must include the sound file in your app, following Apple's guidelines for file type, length and size. See Preparing Custom Alert Sounds.

Once the file is available in your app, simply enter the filename with the extension for the iOS push option sound for the message.

iOS Rich Push setup

Rich push notifications allow you to add images to push notifications.

STEP 1. Create a Notification Service Extension

Within your project, you must create a Service Extension. A Service Extension allows for the modification of a notification to include rich media. To add a notification service extension, click on File > New > Target and select Notification Service Extension.

STEP 2. Setting up Notification Service Extension

Since the Notification Service Extension has it's own bundle id, it must be set up with it's own App ID and provisioning profile. Please verify through the Apple Developer Service.

STEP 3. Add the following code to Notification Service Extension

While you are free to implement your own method within the Notification Service Extension, you will need to make sure that your code is correctly handling the media that is being sent with Leanplum. The key that is associated with the rich media in the Leanplum Payload is: LP_URL.

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        let imageKey = "LP_URL"
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        // MARK: - Leanplum Rich Push
        if let bestAttemptContent = bestAttemptContent {
            let userInfo = request.content.userInfo;
            
            // LP_URL is the key that is used from Leanplum to
            // send the image URL in the payload.
            //
            // If there is no LP_URL in the payload than
            // the code will still show the push notification.
            if userInfo[imageKey] == nil {
                contentHandler(bestAttemptContent);
                return;
            }
            
            // If there is an image in the payload,
            // download and display the image.
            if let attachmentMedia = userInfo[imageKey] as? String {
                let mediaUrl = URL(string: attachmentMedia)
                let LPSession = URLSession(configuration: .default)
                LPSession.downloadTask(with: mediaUrl!, completionHandler: { temporaryLocation, response, error in
                    if let err = error {
                        print("Leanplum: Error with downloading rich push: \(String(describing: err.localizedDescription))")
                        contentHandler(bestAttemptContent);
                        return;
                    }
                    
                    let fileType = self.determineType(fileType: (response?.mimeType)!)
                    let fileName = temporaryLocation?.lastPathComponent.appending(fileType)
                    
                    let temporaryDirectory = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(fileName!)
                    
                    do {
                        try FileManager.default.moveItem(at: temporaryLocation!, to: temporaryDirectory)
                        let attachment = try UNNotificationAttachment(identifier: "", url: temporaryDirectory, options: nil)
                        
                        bestAttemptContent.attachments = [attachment];
                        contentHandler(bestAttemptContent);
                        // The file should be removed automatically from temp
                        // Delete it manually if it is not
                        if FileManager.default.fileExists(atPath: temporaryDirectory.path) {
                            try FileManager.default.removeItem(at: temporaryDirectory)
                        }
                    } catch {
                        print("Leanplum: Error with the rich push attachment: \(error)")
                        contentHandler(bestAttemptContent);
                        return;
                    }
                }).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.
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }
    
    // MARK: - Leanplum Rich Push
    func determineType(fileType: String) -> String {
        // Determines the file type of the attachment to append to URL.
        if fileType == "image/jpeg" {
            return ".jpg";
        }
        if fileType == "image/gif" {
            return ".gif";
        }
        if fileType == "image/png" {
            return ".png";
        } else {
            return ".tmp";
        }
    }

}
@interface NotificationService ()

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

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    NSDictionary *userInfo = request.content.userInfo;
    
    // LP_URL is the key that is used from Leanplum to
    // send the image URL in the payload.
    //
    // If there is no LP_URL in the payload than
    // the code will still show the push notification.
    if (userInfo == nil || userInfo[@"LP_URL"] == nil) {
        self.contentHandler(self.bestAttemptContent);
        return;
    }
    
    NSString *attachmentMedia = userInfo[@"LP_URL"];
    
    // If there is an image in the payload, this part
    // will handle the downloading and displaying of the image.
    if (attachmentMedia) {
        NSURL *URL = [NSURL URLWithString:attachmentMedia];
        NSURLSession *LPSession = [NSURLSession sessionWithConfiguration:
                                   [NSURLSessionConfiguration defaultSessionConfiguration]];
        [[LPSession downloadTaskWithURL:URL completionHandler: ^(NSURL *temporaryLocation, NSURLResponse *response, NSError *error) {
            if (error) {
                NSLog(@"Leanplum: Error with downloading rich push: %@",
                      [error localizedDescription]);
                self.contentHandler(self.bestAttemptContent);
                return;
            }
            
            NSString *fileType = [self determineType: [response MIMEType]];
            NSString *fileName = [[temporaryLocation.path lastPathComponent] stringByAppendingString:fileType];
            NSString *temporaryDirectory = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName];
            [[NSFileManager defaultManager] moveItemAtPath:temporaryLocation.path toPath:temporaryDirectory error:&error];
            
            NSError *attachmentError = nil;
            UNNotificationAttachment *attachment =
            [UNNotificationAttachment attachmentWithIdentifier:@""
                                                           URL:[NSURL fileURLWithPath:temporaryDirectory]
                                                       options:nil
                                                         error:&attachmentError];
            if (attachmentError != NULL) {
                NSLog(@"Leanplum: Error with the rich push attachment: %@",
                      [attachmentError localizedDescription]);
                self.contentHandler(self.bestAttemptContent);
                return;
            }
            self.bestAttemptContent.attachments = @[attachment];
            self.contentHandler(self.bestAttemptContent);
            [[NSFileManager defaultManager] removeItemAtPath:temporaryDirectory error:&error];
        }] resume];
    }
    
}
    
- (NSString*)determineType:(NSString *) fileType {
    // Determines the file type of the attachment to append to NSURL.
    if ([fileType isEqualToString:@"image/jpeg"]){
        return @".jpg";
    }
    if ([fileType isEqualToString:@"image/gif"]) {
        return @".gif";
    }
    if ([fileType isEqualToString:@"image/png"]) {
        return @".png";
    } else {
        return @".tmp";
    }
}
    
- (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);
}

@end

STEP 4. Update didReceiveRemoteNotification

Please make sure to update the application instance method didReceiveRemoteNotification so that it does execute the download of the rich media. See below for a simple example.

func application(_ application: UIApplication,
                         didReceiveRemoteNotification userInfo: [AnyHashable : Any],
                         fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        completionHandler(.newData)
}
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {    
    completionHandler(UIBackgroundFetchResultNewData);
}

STEP 5. Create a Rich Push from our Dashboard

After implementing the code above, you are free to create a rich push notification from our dashboard. See Send a rich push notification for more.

Testing

Once you've set up push notifications, test that it is working properly. Send a push notification to your development devices. Run your app, wait a few seconds, and then press the home button.