ZhgChg.Li

iOS Notification Service Extension|Enhance Push Notifications with Swift

Discover how to implement Notification Service Extension in iOS 10+ to optimize image push notifications, enable pre-display processing, and track notification display stats for improved user engagement.

iOS Notification Service Extension|Enhance Push Notifications with Swift
This article was AI-translated — please let me know if anything looks off.

iOS ≥ 10 Notification Service Extension Application (Swift)

Image Push Notifications, Notification Display Statistics, Pre-processing Before Notification Display

There is plenty of online information about basic push notification setup and principles, so this article will not cover those. The main focus here is how to enable image push notifications in the app and use new features to achieve more accurate push display statistics.

As shown in the above image, the Notification Service Extension allows you to preprocess the notification after the app receives it, before displaying the notification content.

The official documentation states that when processing incoming push notification content, the time limit is about 30 seconds. If the callback is not made within 30 seconds, the notification will continue to be delivered and appear on the user’s device.

Support Level

iOS ≥ 10.0

What can be done in 30 seconds?

  • (Goal 1) Download the image from the image URL field in the push notification content and attach it to the notification 🏆

  • (Goal 2) Track Whether the Notification Was Displayed 🏆

  • Modifying and Reorganizing Push Notification Content

  • Push Notification Content Encryption and Decryption (Decryption) Display

  • Can you decide whether to display the notification? => Answer: No

First, the Payload part of the backend push notification program

The backend must add the line "mutable-content":1 in the push payload for the system to execute the Notification Service Extension upon receiving the push.

{
    "aps": {
        "alert": {
            "title": "New Article Recommended for You",
            "body": "Check it out now"
        },
        "mutable-content":1,
        "sound": "default",
        "badge": 0
    }
}

And… Step 1, Create a New Target for the Project

**Step 1.** Xcode -> File -> New -> Target

Step 1. Xcode -> File -> New -> Target

**Step 2.** iOS -> Notification Service Extension -> Next

Step 2. iOS -> Notification Service Extension -> Next

**Step 3.** Enter Product Name -> Finish

Step 3. Enter Product Name -> Finish

**Step 4.** Click Activate

Step 4. Click Activate

Step 2: Write the Notification Content Handling Code

Find the Product Name/NotificationService.swift file

Find the Product Name/NotificationService.swift file

import UserNotifications

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)
        
        if let bestAttemptContent = bestAttemptContent {
            // Modify the notification content here...
            // Handle push content here, load image back
            bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
            contentHandler(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.
        // Time is running out, just modify the title without the image
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

}

As shown in the code above, NotificationService has two methods; the first is didReceive, which is triggered when a push notification arrives. After processing, you need to call the contentHandler(bestAttemptContent) callback method to notify the system.

If the callback method is not called within the time limit, the second function serviceExtensionTimeWillExpire() will be triggered. At this point, it is basically too late to recover, and you can only perform some cleanup actions (e.g., simply modify the title or content without loading network data).

Practical Example

Here we assume our Payload is as follows

{
    "aps": {
        "alert": {
            "push_id":"2018001",
            "title": "New Article Recommended for You",
            "body": "Check it out now",
            "image": "https://d2uju15hmm6f78.cloudfront.net/image/2016/12/04/3113/2018/09/28/trim_153813426461775700_450x300.jpg"
        },
        "mutable-content":1,
        "sound": "default",
        "badge": 0
    }
}

“push_id” and “image” are custom fields. “push_id” is used to identify the push notification, making it easier for us to send back data to the server for tracking; “image” is the URL of the image to be attached to the push notification.

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    self.contentHandler = contentHandler
    bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    
    if let bestAttemptContent = bestAttemptContent {
        
        guard let info = request.content.userInfo["aps"] as? NSDictionary,let alert = info["alert"] as? Dictionary<String,String> else {
            contentHandler(bestAttemptContent)
            return
            // Notification content format is not as expected, do not process
        }
        
        // Goal 2:
        // Send back to server to notify that the notification was displayed
        if let push_id = alert["push_id"],let url = URL(string: "顯示統計API網址") {
            var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 30)
            request.httpMethod = "POST"
            request.addValue(UserAgent, forHTTPHeaderField: "User-Agent")
            
            var httpBody = "push_id=\(push_id)"
            request.addValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
            request.httpBody = httpBody.data(using: .utf8)
            
            let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
                
            }
            DispatchQueue.global().async {
                task.resume()
                // Handle asynchronously, no need to wait
            }
        }
        
        // Goal 1:
        guard let imageURLString = alert["image"],let imageURL = URL(string: imageURLString) else {
            contentHandler(bestAttemptContent)
            return
            // If no image attached, no special handling needed
        }
        
        
        let dataTask = URLSession.shared.dataTask(with: imageURL) { (data, response, error) in
            guard let fileURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(imageURL.lastPathComponent) else {
                contentHandler(bestAttemptContent)
                return
            }
            guard (try? data?.write(to: fileURL)) != nil else {
                contentHandler(bestAttemptContent)
                return
            }
            
            guard let attachment = try? UNNotificationAttachment(identifier: "image", url: fileURL, options: nil) else {
                contentHandler(bestAttemptContent)
                return
            }
            // Above downloads the image from the URL, saves it locally, and creates a UNNotificationAttachment
            
            bestAttemptContent.categoryIdentifier = "image"
            bestAttemptContent.attachments = [attachment]
            // Add the image attachment to the notification
            
            bestAttemptContent.body = (bestAttemptContent.body == "") ? ("View Now") : (bestAttemptContent.body)
            // If the body is empty, use the default text "View Now"
            
            contentHandler(bestAttemptContent)
        }
        dataTask.resume()
    }
}

I didn’t handle anything special in the serviceExtensionTimeWillExpire part, so I won’t include it; the key is still the didReceive code above.

You can see that when a push notification is received, we first call an API to inform the backend that the notification was received and displayed, which helps with backend push statistics; then, if there is an attached image, we process the image.

When In-App:

It will still trigger Notification Service Extension didReceive, then trigger AppDelegate’s func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any ], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) method.

Note: Regarding image push notifications, you can also….

Use Notification Content Extension to customize the UIView displayed when the notification is pressed (can be custom-made), as well as the press action.

You can refer to this article: iOS10推送通知进阶(Notification Extension)

iOS 12 and later support more action handling: iOS 12 New Notification Features: Adding Interactivity Implementing Complex Functions in Notifications

For the Notification Content Extension part, I only added a UIView that can display image notifications without much refinement:

Wedding App

結婚吧APP

Improve this page
Edit on GitHub
Originally published on Medium
Read the original
Share this essay
Copy link · share to socials
ZhgChgLi
Author

ZhgChgLi

An iOS, web, and automation developer from Taiwan 🇹🇼 who also loves sharing, traveling, and writing.

Comments