flutter 如何设置IOS推送通知的自定义图标?

niwlg2el  于 2023-04-22  发布在  Flutter
关注(0)|答案(2)|浏览(261)

我目前正在为iOS的Flutter应用程序添加通知,我想在Android上添加类似于largeIcon的东西,到目前为止我还没有找到一种方法来做到这一点,只添加一个图像,这不是一个解决方案,因为我只想要一个图标在右边,即使用户扩展通知(图像也会扩展,在这种情况下,这对我的用例来说是不可取的)。
作为替代方案,我想知道我是否可以改变通知应用程序的图标.据我环顾四周,这是不可能的,但在同一时间,我看到这个图像的通知(第一个和最后一个)有一个自定义的图像和应用程序的图标更小.
我如何在我的应用程序中做到这一点?我无法在文档中找到任何方法来做到这一点。

ocebsuys

ocebsuys1#

您可以通过通信通知来实现这一点。来自HIG
系统会自动在每个通知的前沿显示您的应用图标的大版本;在通信通知中,系统会显示发件人的联系人图像(或头像),上面印有您的小图标。
Here是关于如何在应用中实现通信通知的文档。

rn0zuynd

rn0zuynd2#

我不是一个巨大的风扇,只是指向一个文档,因为它是不是真的有帮助的人不熟悉的事情(包括我自己)。所以这里是Flutter的一个完整的功能示例。这肯定也适用于其他iOS应用程序。它遵循Apple Implementing communication notifications提供的文档,并添加了一个实际的准备运行案例。请确保调整您的有效负载/示例有效负载用于演示,但可以与注解的有效负载片段结合使用。

设置和准备您的项目

在你开始之前,你必须做一些准备工作,让一切都开始运行。我假设你已经有一个项目,否则就用flutter create创建一个。

Xcode能力

在你的目标中选择“Runner”并点击+ Capability添加通讯通知功能。这是显示通讯通知所必需的。x1c 0d1x

调整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>

通知服务扩展

1.在Xcode中选择“Runner”-Project
1.转到“文件”=〉“新建”=〉“目标”
1.搜索通知服务扩展并单击“下一步”
1.为您的扩展添加一个软件包名称并点击“完成”
现在我们应该有一个工作项目,其中包含一个代表我们扩展的NotificationService.swift代码文件。这是我们现在可以调整接收到的通知的地方。
我在下面给出了一个可以运行的代码示例,并附上了所有必要的注解。

NotificationService.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
        // 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
    }
}

其他注意事项

我已经添加了额外的注意事项和需要考虑的事项,因为仅仅显示消息通常是不够的,还有其他需要考虑的陷阱。我相信它们会为您节省一些时间,特别是您无法可靠地使用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"
    }
  }
}

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

插件使用

如果你在Flutter App中使用了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,
        );
      }

iOS推送通知附件

默认情况下,只有第一个附件在推送通知中显示为缩略图。请记住这一点。如果您需要更具体的行为,请使用“Rich Notifications”(即通知内容扩展)。

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

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

示例

相关问题