Post

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 post was translated with AI assistance — let me know if anything sounds off!


iOS ≥ 10 Notification Service Extension Application (Swift)

Image Push Notifications, Push Notification Display Statistics, Push Notification Display Preprocessing

Regarding the basics of push notification setup and principles, there is plenty of information available online, so it will not be discussed here. This article focuses on how to enable image push notifications in the app and use new features to achieve more precise push display statistics.

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

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

Support Level

iOS ≥ 10.0

What can you do in 30 seconds?

  • (Goal 1) Download images from the image link field in the push notification content and attach them to the push notification content 🏆

  • (Goal 2) Track whether push notifications are displayed 🏆

  • Push Notification Content Modification and Reorganization

  • Push Notification Content Encryption and Decryption (Decryption) Display

  • Decide whether to show push notifications? => Answer: No

First, the Payload part of the backend push notification program

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

1
2
3
4
5
6
7
8
9
10
11
{
    "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 Push Notification Content Handler

Found Product Name/NotificationService.swift file

Found Product Name/NotificationService.swift file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 notification 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 about to expire, just modify the title without loading image
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

}

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

If the CallBack Method is not called within the time limit, the second function serviceExtensionTimeWillExpire() will be triggered due to timeout. At this point, it’s basically too late to recover, and you can only perform some cleanup tasks (e.g., simply modifying the title or content without loading network data).

Practical Example

Here we assume our Payload is as follows

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "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 both custom fields. push_id is used to identify the push notification for easy tracking and reporting back to the server; image is the URL of the image to be attached to the push notification.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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()
                // Asynchronous processing, ignore result
            }
        }
        
        // Goal 1:
        guard let imageURLString = alert["image"],let imageURL = URL(string: imageURLString) else {
            contentHandler(bestAttemptContent)
            return
            // If no image is 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 reads the image URL, downloads it to the device, 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 default text "View Now"
            
            contentHandler(bestAttemptContent)
        }
        dataTask.resume()
    }
}

I didn’t do anything special with 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 has been received and displayed, which helps with push notification statistics; then, if there is an attached image, we process the image.

When In-App:

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

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

Using Notification Content Extension to customize the UIView displayed when a push notification is pressed (can be custom designed), as well as handling the press action.

You can refer to this article: iOS10 Push Notification Advanced (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 and didn’t put much thought into it:

[結婚吧APP](https://itunes.apple.com/tw/app/%E7%B5%90%E5%A9%9A%E5%90%A7-%E4%B8%8D%E6%89%BE%E6%9C%80%E8%B2%B4-%E5%8F%AA%E6%89%BE%E6%9C%80%E5%B0%8D/id1356057329?ls=1&mt=8){:target="_blank"}

結婚吧APP

If you have any questions or feedback, feel free to contact me.


Buy me a beer

This post was originally published on Medium (View original post), and automatically converted and synced by ZMediumToMarkdown.

Improve this page on Github.

This post is licensed under CC BY 4.0 by the author.