如何修改远程推送显示的图标

1,482 阅读4分钟

这里每天分享一个 iOS 的新知识,快来关注我吧

在之前的文章中,已经介绍过一些关于推送的内容:

1、使用 iOS 模拟器测试推送

2、Xcode 14 模拟器支持远程推送

大概从 iOS 15 开始注意到一些日常使用的 App 的推送可以自定义 logo 了,比如系统电话、FaceTime、iMessage、微博、淘宝等等。

研究了一下方法,这里分享给大家。

这次教程的前提是你的 App 已经支持推送了,如果还不支持推送,需要先配置普通的推送

添加 NotificationService

如果你的应用中还没有添加 NotificationService,需要先添加这个 Target,这是 iOS 10 开始新增的通知服务,可以在不打开 App 的情况下执行一些推送触发的代码

点击顶部导航 File -> New -> Target...

或者是在 Target 下点击加号,也是一样的

然后在弹出的面板上选择 Notification Service Extension 选项,随后点击下一步,输入名字点击确定即可:

添加这个 Target 之后,Xcode 会为我们生成一个新的文件夹,里边有一个 NotificationServiceswift 文件,这个文件一会儿会用到。

Communication Notifications (通讯通知)

除了 Notification Service 之外还需要使用 Communication Notifications(通讯通知),今天教给大家修改推送头像的功能主要就是由这个扩展实现的。

首先选择项目根目录,选中你的 Target,然后在 Signing & Capabilities 部分,点击 + Capability

在弹出的页面上输入 Notification,双击 Communication Notifications 添加进来。

设置 info.plist

在实现通信通知之前,还需要在你的 info.plist 文件中新增一个 NSUserActivityTypes 的数组,里边添加一个 INSendMessageIntent

代码部分

最后就是最重要的代码部分了,打开 NotificationService.swift 文件,可以看到有这样一个方法:

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
}

这个方法会在手机接收到推送的时候调用,因为这个 Extension 是运行在一个独立进程里的,所以即使你的 App 没有启动,这里的代码也会正常执行,很多 App 利用这个特性上报一个推送到达的埋点,以便于统计真实的推送送达率

扯远了,我们需要在这个方法里实现配置自定义图片的代码:

// 将图片下载下来
private func download(url: URL, handler: @escaping (_ data: Data?) -> Void) {
    let task = URLSession.shared.dataTask(with: url, completionHandler: {
        data, _, _ in
        handler(data)
    })
    task.resume()
}

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
    
    self.contentHandler = contentHandler
    bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    
    if #available(iOSApplicationExtension 15.0, *),
       let userInfo = bestAttemptContent?.userInfo,
       let appIconUrl = userInfo["custom_icon_url"] as? String,
       let url = URL(string: appIconUrl) {
        let title = bestAttemptContent?.title
        let subTitle = bestAttemptContent?.subtitle
        let body = bestAttemptContent?.body
        
        download(url: url) { data in
            guard let data = data else {
                return
            }
            
            // Initialize only the sender for a one-to-one message intent.
            let handle = INPersonHandle(value: "", type: .unknown)
            let avatar = INImage(imageData: data)
            let sender = INPerson(personHandle: handle,
                                  nameComponents: PersonNameComponents(),
                                  displayName: title,
                                  image: avatar,
                                  contactIdentifier: nil,
                                  customIdentifier: nil)

            // Because this communication is incoming, you can infer that the current user is
            // a recipient. Don't include the current user when initializing the intent.
            let intent = INSendMessageIntent(recipients: nil,
                                             outgoingMessageType: .outgoingMessageText,
                                             content: body,
                                             speakableGroupName: nil,
                                             conversationIdentifier: nil,
                                             serviceName: nil,
                                             sender: sender,
                                             attachments: nil)
            
            // Use the intent to initialize the interaction.
            let interaction = INInteraction(intent: intent, response: nil)

            // Interaction direction is incoming because the user is
            // receiving this message.
            interaction.direction = .incoming

            // Donate the interaction before updating notification content.
            interaction.donate { error in
                if error != nil {
                    // Handle errors that may occur during donation.
                    return
                }
                
                // After donation, update the notification content.
                let content = request.content
                
                do {
                    // Update notification content before displaying the
                    // communication notification.
                    let updatedContent = try content.updating(from: intent)
                    
                    // Call the content handler with the updated content
                    // to display the communication notification.
                    contentHandler(updatedContent)
                    
                } catch {
                    // Handle errors that may occur while updating content.
                }
            }
        }
        
        return
    }
    
    if let bestAttemptContent = bestAttemptContent {
        contentHandler(bestAttemptContent)
    }
}

代码比较多,稍微解释一下。

didReceive(:, withContentHandler:) 方法会在用户收到推送的时候调用,我们需要在这个方法内做出对推送内容的更改。

首先拿到 push 下发的图片用 download 方法下载下来,生成 INImage 图片对象,然后创建一个 INPerson 对象,之后再创建 INInteraction 实例,最后使用 donate 方法来让本次改动生效。

要推送一条消息,都需要一个 Payload 文件,它是 JSON 格式的,这在以前的文章中也介绍过,我们这里自定义了一个 key:custom_icon_url,它位于最 JSON 的外层:

{
    "aps" : {
        "alert" : {
            "title" : "模拟器推送,标题",
            "subtitle" : "副标题",
            "body" : "这里是推送内容"
        },
        "mutable-content": 1,
        "badge" : 1
    },
    "custom_icon_url": "你的自定义图片链接"
}

按照以上步骤操作,最后就能实现更换推送 Icon 的功能了。

点击下方公众号卡片,关注我,每天分享一个关于 iOS 的新知识

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!