Notification Service Extension

Author - Jayesh

When push notification arrives in an iOS app, you may want to be able to download content in response to it or edit the content before it is shown to the user. In iOS 10 and later, Apple allows apps to do using new Notification Service Extension.

This guide will help you to setting up project to display image in notification banner.

Setting up App Group ID and App ID

Setting up Xcode project

In Xcode, create new app of type Single View App.

Create Notification Service Extension

In the Xcode menu bar, go to File > New > Target… and select the Notification Service Extension template from the menu that appears

After you create extension, you will see two files in Xcode Project navigator

  • NotificationService.swift
    : contains code and logic of extension
  • Info.plist : configuration
    details of extension

Enable main app push notification and app group capabilities

Go to your xcodeproj in Xcode, select Capabilities, and turn ON Push notifications.

and turn ON App Groups.

Enable extension application app group capability

Also turn ON App Groups from extension target.

After enable capabilities entitlements file look like this

(Note: if app group not displaying capabilities then you can add manually from file)

Register push notification in AppDelegate

UNUserNotificationCenter.current().delegate = self
        
if #available(iOS 10, *) {
     UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]){ granted, error in }
} else {
     application.registerUserNotificationSettings(UIUserNotificationSettings(types: [.badge, .sound, .alert], categories: nil))
}
application.registerForRemoteNotifications()

Add the didRegisterForRemoteNotificationsWithDeviceToken function in your AppDelegate.swift file to get the deviceToken, which will receive notifications.

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        
    let deviceTokenString = deviceToken.reduce("") { $0 + String(format: "%02X", $1) }
        
     print("APNs device token: \(deviceTokenString)")
        
     // Persist it in your backend in case it's new
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    completionHandler()
}

Extension Lifecycle

Once you have configured your app with a notification service extension, the following process will take place for each notification.

  • App receives notification.
  • System creates an instance of your extension class and launches it in the background.
  • Your extension performs content edits and/or downloads some content.
  • If your extension takes too long to perform its work, it will be notified and immediately terminated.
  • Notification is displayed to the user.

As you can see, when using a notification service extension, you only have a limited amount of time to perform the necessary work. If your extension is taking too long, then it will be stopped by the system, and the notification will be delivered as if you had no extension at all.

Extension limitation

The last important thing to consider when using a notification service extension is the conditions under which the extension will operate. Firstly, your extension will only be launched for notifications which are configured to show on-screen alerts to the user. This means that any silent notifications (like the ones used to update app badges) will not trigger your extension.

Secondly, the incoming notification’s aps dictionary within its payload must include the mutable-content key with a value of 1.

The correct configuration of your notifications will ultimately depend on your app’s own setup. To meet the second condition, in particular, some changes may need to be made to your server-side push notification implementation. If you are not using a custom server and are using instead a third-party service for your push notification implementation, then I would suggest researching and reading through their support documentation if you can’t get your extension to work.

NotificationService.swift

This file use to handle notification content before notification displayed to user. Once notification arrived, the extension download image from url and display in notification banner. I am storing data in shared user default to access this image into main app without opening app using notification.

When you open app normally last downloaded file will always display in main app.

class NotificationService: UNNotificationServiceExtension {
    
    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)
        let defaults = UserDefaults(suiteName: "group.logistic.test")
        defaults?.set(nil, forKey: "images")
        defaults?.synchronize()
        
        guard let content = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
            contentHandler(request.content)
            return
        }
        
        guard let apnsData = content.userInfo["data"] as? [String: Any] else {
            contentHandler(request.content)
            return
        }
        
        guard let attachmentURL = apnsData["attachment-url"] as? String else {
            contentHandler(request.content)
            return
        }
        
        do {
            let imageData = try Data(contentsOf: URL(string: attachmentURL)!)
            guard let attachment = UNNotificationAttachment.create(imageFileIdentifier: "image.jpg", data: imageData, options: nil) else {
                contentHandler(request.content)
                return
            }
            content.attachments = [attachment]
            contentHandler(content.copy() as! UNNotificationContent)
            
        } catch {
            contentHandler(request.content)
            print("Unable to load data: \(error)")
        }
    }
    
    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)
        }
    }
    
}

This UNNotificationAttachment extension function to save the media to disk.

extension UNNotificationAttachment {
    static func create(imageFileIdentifier: String, data: Data, options: [NSObject : AnyObject]?)
        -> UNNotificationAttachment? {
            let fileManager = FileManager.default
            if let directory = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.logistic.test") {
                do {
                    let newDirectory = directory.appendingPathComponent("Images")
                    if (!fileManager.fileExists(atPath: newDirectory.path)) {
                        try? fileManager.createDirectory(at: newDirectory, withIntermediateDirectories: true, attributes: nil)
                    }
                    let fileURL = newDirectory.appendingPathComponent(imageFileIdentifier)
                    do {
                        try data.write(to: fileURL, options: [])
                    } catch {
                        print("Unable to load data: \(error)")
                    }
                    
                    let defaults = UserDefaults(suiteName: "group.logistic.test")
                    defaults?.set(data, forKey: "images")
                    defaults?.synchronize()
                    let imageAttachment = try UNNotificationAttachment.init(identifier: imageFileIdentifier,
                                                                            url: fileURL,
                                                                            options: options)
                    return imageAttachment
                } catch let error {
                    print("error \(error)")
                }
            }
            return nil
    }
}

In main app, you have to get image when app open

// ViewController.Swift
let defaults = UserDefaults(suiteName: "group.logistic.test")
if (defaults?.object(forKey: "images") != nil) {
let data = defaults?.value(forKey: "images") as! Data
           iimgNotification.image = UIImage(data: data)
}

Sample notification payload

A very important step for sending media attachment is the mutable-content key in the push payload from the server. So let’s take a look at this example payload

{
   "aps": {
       "alert": "Hello!",
       "sound": "default",
       "mutable-content": 1,
       "badge": 1,
   },
   "data": {
       "attachment-url": "https://abc.com/sample_image.jpg"
   }
}

Finally, I Hope this article will help you to build great product. Below you will find GitHub repository with example.

Thanks

Don’t miss the next post!

Loading

Related Posts