如何为iOS推送通知设置自定义图标?

Sas*_*ssa 5 push-notification ios flutter

我目前正在为 IOS 的 Flutter 应用程序添加通知,我想添加类似于largeIconAndroid 上的通知,到目前为止我还没有找到一种方法来做到这一点,只添加一个图像,这不是解决方案,因为我只想即使用户展开通知,右侧也会出现一个图标(在这种情况下图像也会展开,这对于我的用例来说是不合需要的)。

作为替代方案,我想知道是否可以更改通知应用程序图标。据我环顾四周,这也是不可能的,但同时我看到了这张通知图像(第一个和最后一个)具有自定义图像,并且应用程序图标较小。

我如何在我的应用程序中执行此操作?我在文档中找不到任何有关如何执行此操作的方法。

在此输入图像描述

Nig*_*ing 13

我不太喜欢仅仅指向文档,因为它对于不熟悉此事的人(包括我自己)来说并没有真正的帮助。这是 Flutter 的完整功能示例。这肯定也适用于其他 iOS 应用程序。它遵循 Apple 提供的实现通信通知的文档,并添加了一个实际的准备运行案例。确保根据您的需要调整您的有效负载/消息。示例有效负载用于演示,但可以与带注释的有效负载片段结合使用。

设置并准备您的项目

在开始之前,您必须做一些准备工作以使一切正常运行。我假设您已经有一个项目,否则使用flutter create.

Xcode 功能

在目标中选择“Runner”,然后点击 来添加通信通知功能+ Capability。这是显示通信通知所必需的。 添加通信通知功能

Info.plist 的调整

转到ios\Runner\Info.plist并添加键NSUserActivityTypesINSendMessageIntent值。这允许我们使用 SendMessageIntent,因为我们基本上向自己发送一条消息。注意:我删除了所有其他条目只是为了避免混乱我的答案。将密钥及其数组附加到您的 Info.plist 中,并且不要删除其他任何内容!

信息表

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>NSUserActivityTypes</key>
        <array>
            <string>INSendMessageIntent</string>
        </array>
    </dict>
</plist>
Run Code Online (Sandbox Code Playgroud)

通知服务扩展

  1. 在 Xcode 中选择“Runner”-Project
  2. 转到“文件”=>“新建”=>“目标”
  3. 搜索通知服务扩展并点击“下一步”
  4. 为您的扩展添加包名称并点击“完成”

现在我们应该有一个工作项目,其中包含NotificationService.swift代表我们的扩展的代码文件。现在我们可以在这里调整收到的通知。

我已经在下面转储了准备运行的代码示例以及所有必要的注释。请关注评论以获得更好的理解。

通知服务.swift

import UserNotifications
import Intents

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?

    /// This is called when a notification is received.
    /// - Parameters:
    ///   - request: The notification request.
    ///   - contentHandler: The callback that needs to be called when the notification is ready to be displayed.
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler


        // the OS already did some work for us so we do make a work copy. If something does not go the way we expect or we run in a timeout we still have an attempt that can be displayed.
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)

        // unwrapping makes the compiler happy
        if(bestAttemptContent == nil) {
            return;
        }

        // this is the FCM / APNS payload defined by the server / caller. 
        // Its the custom '"data"' object you provide in the FCM json.
        // adjust it to your needs. This is just an example.
        let payload: [AnyHashable : Any] = bestAttemptContent!.userInfo

        // we assume that we get a type in the payload
        // either remove this line or add a type to your payload
        let type: String? = payload["type"] as? String

        // this is set by the server to indicate that this is a chat message
        if(type == "chat") {
            _handleChatMessage(payload: payload)

            return;
        }

        // if we do not know the type we just pass the notification through
        // this is the case when we get a plain FCM / APNS notification
        if let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

    /// Handles a chat message notification. It tries to display it as a communication notification.
    /// - Parameter payload: The FCM / APNS payload, defined by the server / caller.
    func _handleChatMessage(payload: [AnyHashable : Any]) {
        guard let content = bestAttemptContent else {
            return
        }

        guard let contentHandler = contentHandler else {
            return
        }

        // add your custom logic here. Read the payload information and act accordingly.
        // all following code assumes you provide this information in the payload.

        // let chatRoomName: String? = payload["chatRoomName"] as? String

        // guard let chatRoomName: String = chatRoomName, !chatRoomName.isEmpty else {
        //     return
        // }

        let chatRoomName: String = "My Custom Room" // this can be a senders name or the name of a channel or group

        let senderId: String = "f91840a2-a1bd-4d7a-a7ea-b4c08f7292e0" // use whatever value you have from your backend

        let senderDisplayName: String = "Sender A"

        let senderThumbnail: String = "https://picsum.photos/300"

        guard let senderThumbnailUrl: URL = URL(string: senderThumbnail) else {
            return
        }

        let senderThumbnailFileName: String = senderThumbnailUrl.lastPathComponent // we grab the last part in the hope it contains the actual filename (any-picture.jpg)

        guard let senderThumbnailImageData: Data = try? Data(contentsOf: senderThumbnailUrl),
            let senderThumbnailImageFileUrl: URL = try? downloadAttachment(data: senderThumbnailImageData, fileName: senderThumbnailFileName),
            let senderThumbnailImageFileData: Data = try? Data(contentsOf: senderThumbnailImageFileUrl) else {

            return
        }

        // example for adding attachments. Will be displayed by the communication notification.
        var attachments: [UNNotificationAttachment] = [];

        // Note: TODO -> Make sure that it has a file extension. Otherwise the creation of an attachment will fail and return nil.
        let link: String? = "https://fastly.picsum.photos/id/368/536/354.jpg?hmac=2b0UU6Y-8XxkiRBhatgBJ-ni3aWJ5CcVVENpX-mEiIA" // payload["link"] as? String

        if let link = link, !link.isEmpty {
            let url = URL(string: link)
            let fileName = url!.lastPathComponent // same here => we hope it contains a proper file extension.

            let imageData = try? Data(contentsOf: url!)

            if(imageData != nil) {
                let attachment = createNotificationAttachment(identifier: "media", fileName: fileName, data: imageData!, options: nil)

                if attachment != nil {
                    attachments.append(attachment!)
                }
            }
        }

        // Add a preview to the notification.
        // Maybe the sender attached a picture or a video.
        // Handle attachments here before converting it to a communication notification
        // as I had issues when trying adding attachments afterwards.
        // Note: Those can be reused in the Notification Content Extension
        content.attachments = attachments

        // profile picture that will be displayed in the notification (left side)
        let senderAvatar: INImage = INImage(imageData: senderThumbnailImageFileData)

        var personNameComponents = PersonNameComponents()
        personNameComponents.nickname = senderDisplayName

        // the person that sent the message
        // we need that as it is used by the OS trying to identify/match the sender with a contact
        // Setting ".unknown" as type will prevent the OS from trying to match the sender with a contact
        // as here this is an internal identifier and not a phone number or email
        let senderPerson = INPerson(
                                    personHandle: INPersonHandle(
                                                    value: senderId,
                                                    type: .unknown
                                                ),
                                    nameComponents: personNameComponents,
                                    displayName: senderDisplayName,
                                    image: senderAvatar,
                                    contactIdentifier: nil,
                                    customIdentifier: nil,
                                    isMe: false, // this makes the OS recognize this as a sender
                                    suggestionType: .none
                                )

        // this is just a dummy person that will be used as the recipient
        let selfPerson = INPerson(
                                    personHandle: INPersonHandle(
                                                    value: "00000000-0000-0000-0000-000000000000", // no need to set a real value here
                                                    type: .unknown
                                                ),
                                    nameComponents: nil,
                                    displayName: nil,
                                    image: nil,
                                    contactIdentifier: nil,
                                    customIdentifier: nil,
                                    isMe: true, // this makes the OS recognize this as "US"
                                    suggestionType: .none
                                )

        // the actual message. We use the OS to send us ourselves a message.
        let incomingMessagingIntent = INSendMessageIntent(
                                            recipients: [selfPerson],
                                            outgoingMessageType: .outgoingMessageText, // This marks the message as outgoing
                                            content: content.body, // this will replace the content.body
                                            speakableGroupName: nil,
                                            conversationIdentifier: chatRoomName, // this will be used as the conversation title
                                            serviceName: nil,
                                            sender: senderPerson, // this marks the message sender as the person we defined above
                                            attachments: []
                                    )

        incomingMessagingIntent.setImage(senderAvatar, forParameterNamed: \.sender)

        let interaction = INInteraction(intent: incomingMessagingIntent, response: nil)

        interaction.direction = .incoming

        do {
            // we now update / patch / convert our attempt to a communication notification.
            bestAttemptContent = try content.updating(from: incomingMessagingIntent) as? UNMutableNotificationContent

            // everything went alright, we are ready to display our notification.
            contentHandler(bestAttemptContent!)
        } catch let error {
            print("error \(error)")
        }
    }

    /// 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.
    override func serviceExtensionTimeWillExpire() {
        if let contentHandler = contentHandler, let bestAttemptContent =  bestAttemptContent {
            contentHandler(bestAttemptContent)
        }
    }

    /// Shorthand for creating a notification attachment.
    /// - Parameters:
    ///   - identifier: Unique identifier for the attachment. So it can be referenced within a Notification Content extension for example.
    ///   - fileName: The name of the file. This is the name that will be used to store the name on disk.
    ///   - data: A Data object based on the remote url.
    ///   - options: A dictionary of options. See Apple's documentation for more information.
    /// - Returns: A UNNotificationAttachment object.
    func createNotificationAttachment(identifier: String, fileName: String, data: Data, options: [NSObject : AnyObject]?) -> UNNotificationAttachment? {
        do {
           if let fileURL: URL = downloadAttachment(data: data, fileName: fileName) {
                let attachment: UNNotificationAttachment = try UNNotificationAttachment.init(identifier: identifier, url: fileURL, options: options)

                return attachment
            }

            return nil
        } catch let error {
            print("error \(error)")
        }

        return nil
    }

    /// Downloads a file from a remote url and stores it in a temporary folder.
    /// - Parameters:
    ///   - data: A Data object based on the remote url.
    ///   - fileName: The name of the file. This is the name that will be used to store the name on disk.
    /// - Returns: A URL object pointing to the temporary file on the phone. This can be used by a Notification Content extension for example.
    func downloadAttachment(data: Data, fileName: String) -> URL? {
        // Create a temporary file URL to write the file data to
        let fileManager = FileManager.default
        let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
        let tmpSubFolderURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(tmpSubFolderName, isDirectory: true)

        do {
            // prepare temp subfolder
            try fileManager.createDirectory(at: tmpSubFolderURL, withIntermediateDirectories: true, attributes: nil)
            let fileURL: URL = tmpSubFolderURL.appendingPathComponent(fileName)

            // Save the image data to the local file URL
            try data.write(to: fileURL)

            return fileURL
        } catch let error {
            print("error \(error)")
        }

        return nil
    }
}

Run Code Online (Sandbox Code Playgroud)

需要考虑的其他事项

我添加了额外的注释和需要考虑的事项,因为仅显示消息通常是不够的,并且还有其他陷阱需要考虑。我相信它们会为您节省一些时间,尤其是您无法可靠地使用 iOS 上的静默通知的部分(这就是我们必须使用通知服务扩展的原因)。

FCM 有效负载/静默推送

确保您确实使用 iOS / APNS 的实际警报,因为在应用程序处于非活动状态一定时间后,不会处理静默通知。在 Flutter 上,它会工作到一定程度,然后停止工作。这是设计使然,但对于新手来说这是一个巨大的陷阱,因为直到用户抱怨他们在一定时间后没有收到通知时你才会意识到它。您可以使用 Playground 生成令牌并在使用 FCM 时测试您的实现:Google OAuth Playground

FCM 的有效负载示例

{
  "message": {
    "token": "TOKEN retrieved from Google OAuth Playground",
    "apns": {
      "payload": {
        "aps": {
          "alert": {
            "title": "Sender A",
            "body": "Hello Service Extension"
          },
          "mutable-content": 1,
          "content-available": 1,
          "sound": "default"
        }
      }
    },
    "data": {
      "note": "This is an example payload!",
      "type": "chat",
      "chatRoomName": "My Custom Room",
      "senderId": "f91840a2-a1bd-4d7a-a7ea-b4c08f7292e0",
      "senderDisplayName": "Sender A",
      "senderThumbnail": "https://picsum.photos/300",
      "link": "https://fastly.picsum.photos/id/368/536/354.jpg?hmac=2b0UU6Y-8XxkiRBhatgBJ-ni3aWJ5CcVVENpX-mEiIA"
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

您仍然可以在 Android 上使用静默通知,因为即使在终止状态下它们也会触发您的应用程序。这样您就可以在 Android 上使用 Flutter 插件,并在平台为 iOS 时禁用它们。或者您可以使用组合并根据您的情况打开/关闭前景可视化。

插件的使用

如果您确实在 Flutter 应用程序中使用了像flutter_local_notificationsfirebase_messaging这样的插件,请注意,当“mutable-content”设置为 1 时,会调用该扩展。因此,如果您看到通知两次,甚至在不同的布局中,那么您必须禁用在前台显示演示文稿,或者在平台为 iOS 时放弃对 flutter_local_notifications 的调用,并让您的扩展处理可视化。

禁用前台通知

 if (Platform.isIOS) {
        // disable foreground notifications to make sure the app extension handles them
        await FirebaseMessaging.instance.setForegroundNotificationPresentationOptions(
          alert: false,
          badge: false,
          sound: false,
        );
      }
Run Code Online (Sandbox Code Playgroud)

iOS 推送通知附件

使用默认实现时,只有第一个附件在推送通知中显示为缩略图。记住这一点。如果您需要更具体的行为,请使用“丰富通知”,又称通知内容扩展。

所有通知都将调用您的分机!

另请记住,您的应用程序将收到的所有通知都会调用您的扩展程序。例如,在扩展中使用类别标识符 (request.content.categoryIdentifier) 来过滤或提交有效负载中的类型(就像我所做的那样)。

例子

例子


ole*_*.le 2

您可以通过通信通知来做到这一点。来自HIG

\n
\n

系统会自动在每个通知的前缘\n显示较大版本的应用图标;在通信通知中,系统会显示发件人\xe2\x80\x99 的联系人图像\xe2\x80\x94 或\navatar \xe2\x80\x94,并带有您的图标的小版本标记。

\n
\n

以下是有关如何在应用程序中实现通信通知的文档。

\n