Post

iOS Notification Service Extension|Swift 实作图片推播与显示统计技巧

针对iOS 10以上推播,实现图片下载附加与显示统计,解决推播无法即时显示图片与追踪点击的痛点,透过Notification Service Extension提升推播互动与精准度,助你优化用户体验与后台数据分析。

iOS Notification Service Extension|Swift 实作图片推播与显示统计技巧

Click here to view the English version of this article.

點擊這裡查看本文章正體中文版本。

基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。


iOS ≥ 10 Notification Service Extension 应用 (Swift)

图片推播、推播显示统计、推播显示前处理

关于基础的推播建置、推播原理;网路资料很多,这边就不再论述,本篇主要重点在如何让APP支援图片推播及运用新特性达成更精准的推播显示统计.

如上图所示,Notification Service Extension让你在APP收到推播后能针对推播做预处理,然后才显示推播内容

官方文件写到,我们针对推播进来的内容做处理时,处理时限大约30秒钟,如果超过30秒还没CallBack,推播就会继续执行,出现在使用者的手机.

支援度

iOS ≥ 10.0

30秒可以干嘛?

  • (目标1) 从推播内容的图片连结栏位下载图片回来,并附加到推播内容上🏆

  • (目标2) 统计推播有无显示🏆

  • 推播内容修改、重组内容

  • 推播内容加解密(解密)显示

  • 决定推播要不要显示? =>> 答案:不行

首先,后端推播程式的 Payload 部分

后端在推播时的结构要多加上一行 “mutable-content":1 系统收到推播才会执行Notification Service Extension

1
2
3
4
5
6
7
8
9
10
11
{
    "aps": {
        "alert": {
            "title": "新文章推荐给您",
            "body": "立即查看"
        },
        "mutable-content":1,
        "sound": "default",
        "badge": 0
    }
}

And… 第一步,为专案新建一个Target

**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.** 输入Product Name -> Finish

Step 3. 输入Product Name -> Finish

**Step 4.** 点选 Activate

Step 4. 点选 Activate

第二步,撰写推播内容处理程式

找到Product Name/NotificationService.swift档

找到Product Name/NotificationService.swift档

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...
            // 推播内容在这处理,Load 图片回来
            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.
        // 要逾时了,不管图片 只改标题内容就好
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

}

如上程式码,NotificationService有两个接口;第一个是 didReceive 当有推播进来时会触发这个function,其中当处理完毕后需要呼叫 contentHandler(bestAttemptContent) 这个CallBack Method告知系统

如果时间过久都没呼叫CallBack Method,就会触发第二个 function serviceExtensionTimeWillExpire() 已逾时,基本上已回天乏术,只能做一些收尾的动作(例如:单纯改改标题、内容,不Load网路资料了)

实战范例

这里假设我们的 Payload 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
{
    "aps": {
        "alert": {
            "push_id":"2018001",
            "title": "新文章推荐给您",
            "body": "立即查看",
            "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」跟「image」都是我自订的栏位,push_id用于辨识推播方便我们传回伺服器做统计;image 则是推播要附加的图片内容之图片网址

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
            //推播内容格式不如预期,不处理
        }
        
        //目标2:
        //回传Server,告知推播有显示
        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()
                //异步处理,不管他
            }
        }
        
        //目标1:
        guard let imageURLString = alert["image"],let imageURL = URL(string: imageURLString) else {
            contentHandler(bestAttemptContent)
            return
            //若无附图片,则不用特别处理
        }
        
        
        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
            }
            //以上为读取图片连结并下载到手机并放入建立UNNotificationAttachment
            
            bestAttemptContent.categoryIdentifier = "image"
            bestAttemptContent.attachments = [attachment]
            //为推播添加附件图片
            
            bestAttemptContent.body = (bestAttemptContent.body == "") ? ("立即查看") : (bestAttemptContent.body)
            //如果body为空,则用预设内容"立即查看"
            
            contentHandler(bestAttemptContent)
        }
        dataTask.resume()
    }
}

serviceExtensionTimeWillExpire 的部分我没特别处理什么,就不贴了;关键还是上述 didReceive 的程式码

可以看到当接受到有推播通知时,我们先Call Api告诉后端有收到并将显示推播了,方便我们后台做推播统计;然后若有附加图片再对图片进行处理.

In-App状态时:

ㄧ样会触发Notification Service Extension didReceive 再触发AppDelegate的 func application( _ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any ], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) 方法

附注:关于图片推播的部分你还可以….

使用 Notification Content Extension 自订推播按压时要显示的UIView(可以自己刻),还有按压的动作

可参考这篇: iOS10推送通知进阶(Notification Extension)

iOS 12之后支援更多动作处理: iOS 12 新通知功能:添加互动性 在通知中实作复杂功能

Notification Content Extension的部分,我只拉了一个能展示图片推播的UIView 并没有做太多琢磨:

[结婚吧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

有任何问题及指教欢迎 与我联络


Buy me a beer

本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。

Improve this page on Github.

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