mirror of
https://github.com/Finb/Bark.git
synced 2025-12-08 21:36:01 +00:00
重构NotificationService
This commit is contained in:
parent
f509cbd5d4
commit
0238cacf5d
@ -126,6 +126,17 @@
|
||||
06C595362481160F006B98F3 /* BKLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06C595352481160F006B98F3 /* BKLabel.swift */; };
|
||||
06CF784721C7A50300A052D7 /* NotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06CF784B21C7A51200A052D7 /* NotificationService.swift */; };
|
||||
06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */; };
|
||||
06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */; };
|
||||
06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */; };
|
||||
06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */; };
|
||||
06E944712C06FF4C00AC86AB /* AutoCopyProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */; };
|
||||
06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */; };
|
||||
06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944742C07012E00AC86AB /* RealmConfiguration.swift */; };
|
||||
06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944742C07012E00AC86AB /* RealmConfiguration.swift */; };
|
||||
06E944782C0701F300AC86AB /* IconProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944772C0701F300AC86AB /* IconProcessor.swift */; };
|
||||
06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E944792C0704E500AC86AB /* ImageDownloader.swift */; };
|
||||
06E9447C2C07052F00AC86AB /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06E9447B2C07052F00AC86AB /* ImageProcessor.swift */; };
|
||||
06EE1FD326843E9300586708 /* BarkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EE1FD226843E9300586708 /* BarkTests.swift */; };
|
||||
06EEF333291CCFF400CA228A /* CryptoSettingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF332291CCFF400CA228A /* CryptoSettingController.swift */; };
|
||||
06EEF335291CD00000CA228A /* CryptoSettingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 06EEF334291CD00000CA228A /* CryptoSettingViewModel.swift */; };
|
||||
@ -305,6 +316,16 @@
|
||||
06CF784021C7A50300A052D7 /* NotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
06CF784421C7A50300A052D7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
06CF784B21C7A51200A052D7 /* NotificationService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentProcessor.swift; sourceTree = "<group>"; };
|
||||
06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CiphertextProcessor.swift; sourceTree = "<group>"; };
|
||||
06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LevelProcessor.swift; sourceTree = "<group>"; };
|
||||
06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeProcessor.swift; sourceTree = "<group>"; };
|
||||
06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCopyProcessor.swift; sourceTree = "<group>"; };
|
||||
06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArchiveProcessor.swift; sourceTree = "<group>"; };
|
||||
06E944742C07012E00AC86AB /* RealmConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealmConfiguration.swift; sourceTree = "<group>"; };
|
||||
06E944772C0701F300AC86AB /* IconProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconProcessor.swift; sourceTree = "<group>"; };
|
||||
06E944792C0704E500AC86AB /* ImageDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDownloader.swift; sourceTree = "<group>"; };
|
||||
06E9447B2C07052F00AC86AB /* ImageProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageProcessor.swift; sourceTree = "<group>"; };
|
||||
06EE1FD026843E9300586708 /* BarkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BarkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
06EE1FD226843E9300586708 /* BarkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BarkTests.swift; sourceTree = "<group>"; };
|
||||
06EE1FD426843E9300586708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@ -512,6 +533,7 @@
|
||||
0653677729B727A60038BDB8 /* CryptoSettingRelay.swift */,
|
||||
06F08EA629B1DDFE006AB9CA /* Error+Extension.swift */,
|
||||
06F08EAB29B1DECD006AB9CA /* NSLocalizedString+Extension.swift */,
|
||||
06E944742C07012E00AC86AB /* RealmConfiguration.swift */,
|
||||
);
|
||||
path = Common;
|
||||
sourceTree = "<group>";
|
||||
@ -571,6 +593,7 @@
|
||||
06CF784121C7A50300A052D7 /* NotificationServiceExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
06E9446B2C06E4A800AC86AB /* Processor */,
|
||||
06B11590247BBC15006D91FB /* NotificationServiceExtension.entitlements */,
|
||||
06CF784B21C7A51200A052D7 /* NotificationService.swift */,
|
||||
06CF784421C7A50300A052D7 /* Info.plist */,
|
||||
@ -578,6 +601,22 @@
|
||||
path = NotificationServiceExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
06E9446B2C06E4A800AC86AB /* Processor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
06E944672C06E40600AC86AB /* NotificationContentProcessor.swift */,
|
||||
06E944692C06E4A200AC86AB /* CiphertextProcessor.swift */,
|
||||
06E9446C2C06FEC900AC86AB /* LevelProcessor.swift */,
|
||||
06E9446E2C06FF1E00AC86AB /* BadgeProcessor.swift */,
|
||||
06E944702C06FF4C00AC86AB /* AutoCopyProcessor.swift */,
|
||||
06E944722C06FF9200AC86AB /* ArchiveProcessor.swift */,
|
||||
06E944772C0701F300AC86AB /* IconProcessor.swift */,
|
||||
06E9447B2C07052F00AC86AB /* ImageProcessor.swift */,
|
||||
06E944792C0704E500AC86AB /* ImageDownloader.swift */,
|
||||
);
|
||||
path = Processor;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
06EE1FD126843E9300586708 /* BarkTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -981,6 +1020,7 @@
|
||||
0667D194247D1BA0005DE2ED /* Date+Extension.swift in Sources */,
|
||||
0604F7DF20620D4900B32F09 /* ServerManager.swift in Sources */,
|
||||
0667D192247D162C005DE2ED /* MessageTableViewCell.swift in Sources */,
|
||||
06E944762C07013000AC86AB /* RealmConfiguration.swift in Sources */,
|
||||
0603706720E1E31600F4CA05 /* Defines.swift in Sources */,
|
||||
06787C3B2AB82BDB008ABDD7 /* CrashReportViewController.swift in Sources */,
|
||||
064CAB9E256BE9090018155C /* PreviewCardCellViewModel.swift in Sources */,
|
||||
@ -1015,12 +1055,22 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
06E944752C07012E00AC86AB /* RealmConfiguration.swift in Sources */,
|
||||
06E944732C06FF9200AC86AB /* ArchiveProcessor.swift in Sources */,
|
||||
06E9447A2C0704E500AC86AB /* ImageDownloader.swift in Sources */,
|
||||
06CF784C21C7A51200A052D7 /* NotificationService.swift in Sources */,
|
||||
06F08EAD29B1DED6006AB9CA /* NSLocalizedString+Extension.swift in Sources */,
|
||||
0653677629B719BC0038BDB8 /* CryptoSettingManager.swift in Sources */,
|
||||
06E9446F2C06FF1E00AC86AB /* BadgeProcessor.swift in Sources */,
|
||||
06E9446D2C06FEC900AC86AB /* LevelProcessor.swift in Sources */,
|
||||
06E944682C06E40600AC86AB /* NotificationContentProcessor.swift in Sources */,
|
||||
06F08EA529B1DDA7006AB9CA /* Algorithm.swift in Sources */,
|
||||
06E944782C0701F300AC86AB /* IconProcessor.swift in Sources */,
|
||||
06BBB89125650CCF0076F63E /* ArchiveSettingManager.swift in Sources */,
|
||||
06B11591247BC132006D91FB /* Message.swift in Sources */,
|
||||
06E9447C2C07052F00AC86AB /* ImageProcessor.swift in Sources */,
|
||||
06E9446A2C06E4A200AC86AB /* CiphertextProcessor.swift in Sources */,
|
||||
06E944712C06FF4C00AC86AB /* AutoCopyProcessor.swift in Sources */,
|
||||
06F08EA829B1DE0A006AB9CA /* Error+Extension.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@ -11,7 +11,6 @@ import CrashReporter
|
||||
import IceCream
|
||||
import IQKeyboardManagerSwift
|
||||
import Material
|
||||
import RealmSwift
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
@ -20,22 +19,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
var window: UIWindow?
|
||||
var syncEngine: SyncEngine?
|
||||
func setupRealm() {
|
||||
let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")
|
||||
let fileUrl = groupUrl?.appendingPathComponent("bark.realm")
|
||||
let config = Realm.Configuration(
|
||||
fileURL: fileUrl,
|
||||
schemaVersion: 13,
|
||||
migrationBlock: { _, oldSchemaVersion in
|
||||
// We haven’t migrated anything yet, so oldSchemaVersion == 0
|
||||
if oldSchemaVersion < 1 {
|
||||
// Nothing to do!
|
||||
// Realm will automatically detect new properties and removed properties
|
||||
// And will update the schema on disk automatically
|
||||
}
|
||||
}
|
||||
)
|
||||
// Tell Realm to use this new configuration object for the default Realm
|
||||
Realm.Configuration.defaultConfiguration = config
|
||||
Realm.Configuration.defaultConfiguration = kRealmDefaultConfiguration
|
||||
|
||||
// iCloud 同步
|
||||
syncEngine = SyncEngine(objects: [
|
||||
|
||||
28
Common/RealmConfiguration.swift
Normal file
28
Common/RealmConfiguration.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// RealmConfiguration.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
@_exported import RealmSwift
|
||||
import UIKit
|
||||
|
||||
let kRealmDefaultConfiguration = {
|
||||
let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")
|
||||
let fileUrl = groupUrl?.appendingPathComponent("bark.realm")
|
||||
let config = Realm.Configuration(
|
||||
fileURL: fileUrl,
|
||||
schemaVersion: 13,
|
||||
migrationBlock: { _, oldSchemaVersion in
|
||||
// We haven’t migrated anything yet, so oldSchemaVersion == 0
|
||||
if oldSchemaVersion < 1 {
|
||||
// Nothing to do!
|
||||
// Realm will automatically detect new properties and removed properties
|
||||
// And will update the schema on disk automatically
|
||||
}
|
||||
}
|
||||
)
|
||||
return config
|
||||
}()
|
||||
@ -6,334 +6,36 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Intents
|
||||
import Kingfisher
|
||||
import MobileCoreServices
|
||||
import RealmSwift
|
||||
import SwiftyJSON
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
lazy var realm: Realm? = {
|
||||
let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")
|
||||
let fileUrl = groupUrl?.appendingPathComponent("bark.realm")
|
||||
let config = Realm.Configuration(
|
||||
fileURL: fileUrl,
|
||||
schemaVersion: 13,
|
||||
migrationBlock: { _, oldSchemaVersion in
|
||||
// We haven’t migrated anything yet, so oldSchemaVersion == 0
|
||||
if oldSchemaVersion < 1 {
|
||||
// Nothing to do!
|
||||
// Realm will automatically detect new properties and removed properties
|
||||
// And will update the schema on disk automatically
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Tell Realm to use this new configuration object for the default Realm
|
||||
Realm.Configuration.defaultConfiguration = config
|
||||
return try? Realm()
|
||||
}()
|
||||
|
||||
/// 自动保存推送
|
||||
/// - Parameters:
|
||||
/// - userInfo: 推送参数
|
||||
/// - bestAttemptContentBody: 推送body,如果用户`没有指定要复制的值` ,默认复制 `推送正文`
|
||||
fileprivate func autoCopy(_ userInfo: [AnyHashable: Any], defaultCopy: String) {
|
||||
if userInfo["autocopy"] as? String == "1"
|
||||
|| userInfo["automaticallycopy"] as? String == "1"
|
||||
{
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
}
|
||||
else {
|
||||
UIPasteboard.general.string = defaultCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存推送
|
||||
/// - Parameter userInfo: 推送参数
|
||||
/// 如果用户携带了 `isarchive` 参数,则以 `isarchive` 参数值为准
|
||||
/// 否则,以用户`应用内设置`为准
|
||||
fileprivate func archive(_ userInfo: [AnyHashable: Any]) {
|
||||
var isArchive: Bool?
|
||||
if let archive = userInfo["isarchive"] as? String {
|
||||
isArchive = archive == "1" ? true : false
|
||||
}
|
||||
if isArchive == nil {
|
||||
isArchive = ArchiveSettingManager.shared.isArchive
|
||||
}
|
||||
let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any]
|
||||
let title = alert?["title"] as? String
|
||||
let body = alert?["body"] as? String
|
||||
|
||||
let url = userInfo["url"] as? String
|
||||
let group = userInfo["group"] as? String
|
||||
|
||||
if isArchive == true {
|
||||
try? realm?.write {
|
||||
let message = Message()
|
||||
message.title = title
|
||||
message.body = body
|
||||
message.url = url
|
||||
message.group = group
|
||||
message.createDate = Date()
|
||||
realm?.add(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 保存图片到缓存中
|
||||
/// - Parameters:
|
||||
/// - cache: 使用的缓存
|
||||
/// - data: 图片 Data 数据
|
||||
/// - key: 缓存 Key
|
||||
func storeImage(cache: ImageCache, data: Data, key: String) async {
|
||||
return await withCheckedContinuation { continuation in
|
||||
cache.storeToDisk(data, forKey: key, expiration: StorageExpiration.never) { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用 Kingfisher.ImageDownloader 下载图片
|
||||
/// - Parameter url: 下载的图片URL
|
||||
/// - Returns: 返回 Result
|
||||
func downloadImage(url: URL) async -> Result<ImageLoadingResult, KingfisherError> {
|
||||
return await withCheckedContinuation { continuation in
|
||||
Kingfisher.ImageDownloader.default.downloadImage(with: url, options: nil) { result in
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载推送图片
|
||||
/// - Parameter imageUrl: 图片URL字符串
|
||||
/// - Returns: 保存在本地中的`图片 File URL`
|
||||
fileprivate func downloadImage(_ imageUrl: String) async -> String? {
|
||||
guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark"),
|
||||
let cache = try? ImageCache(name: "shared", cacheDirectoryURL: groupUrl),
|
||||
let imageResource = URL(string: imageUrl)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 先查看图片缓存
|
||||
if cache.diskStorage.isCached(forKey: imageResource.cacheKey) {
|
||||
return cache.cachePath(forKey: imageResource.cacheKey)
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
guard let result = try? await downloadImage(url: imageResource).get() else {
|
||||
return nil
|
||||
}
|
||||
// 缓存图片
|
||||
await storeImage(cache: cache, data: result.originalData, key: imageResource.cacheKey)
|
||||
|
||||
return cache.cachePath(forKey: imageResource.cacheKey)
|
||||
}
|
||||
|
||||
/// 为 Notification Content 设置图片
|
||||
/// - Parameter bestAttemptContent: 要设置的 Notification Content
|
||||
/// - Returns: 返回设置图片后的 Notification Content
|
||||
fileprivate func setImage(content bestAttemptContent: UNMutableNotificationContent) async -> UNMutableNotificationContent {
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
guard let imageUrl = userInfo["image"] as? String,
|
||||
let imageFileUrl = await downloadImage(imageUrl)
|
||||
else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
let copyDestUrl = URL(fileURLWithPath: imageFileUrl).appendingPathExtension(".tmp")
|
||||
// 将图片缓存复制一份,推送使用完后会自动删除,但图片缓存需要留着以后在历史记录里查看
|
||||
try? FileManager.default.copyItem(
|
||||
at: URL(fileURLWithPath: imageFileUrl),
|
||||
to: copyDestUrl
|
||||
)
|
||||
|
||||
if let attachment = try? UNNotificationAttachment(
|
||||
identifier: "image",
|
||||
url: copyDestUrl,
|
||||
options: [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]
|
||||
) {
|
||||
bestAttemptContent.attachments = [attachment]
|
||||
}
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
/// 为 Notification Content 设置ICON
|
||||
/// - Parameter bestAttemptContent: 要设置的 Notification Content
|
||||
/// - Returns: 返回设置ICON后的 Notification Content
|
||||
fileprivate func setIcon(content bestAttemptContent: UNMutableNotificationContent) async -> UNMutableNotificationContent {
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
|
||||
guard let imageUrl = userInfo["icon"] as? String,
|
||||
let imageFileUrl = await downloadImage(imageUrl)
|
||||
else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
var personNameComponents = PersonNameComponents()
|
||||
personNameComponents.nickname = bestAttemptContent.title
|
||||
|
||||
let avatar = INImage(imageData: NSData(contentsOfFile: imageFileUrl)! as Data)
|
||||
let senderPerson = INPerson(
|
||||
personHandle: INPersonHandle(value: "", type: .unknown),
|
||||
nameComponents: personNameComponents,
|
||||
displayName: personNameComponents.nickname,
|
||||
image: avatar,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil,
|
||||
isMe: false,
|
||||
suggestionType: .none
|
||||
)
|
||||
let mePerson = INPerson(
|
||||
personHandle: INPersonHandle(value: "", type: .unknown),
|
||||
nameComponents: nil,
|
||||
displayName: nil,
|
||||
image: nil,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil,
|
||||
isMe: true,
|
||||
suggestionType: .none
|
||||
)
|
||||
|
||||
let intent = INSendMessageIntent(
|
||||
recipients: [mePerson],
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: bestAttemptContent.body,
|
||||
speakableGroupName: INSpeakableString(spokenPhrase: personNameComponents.nickname ?? ""),
|
||||
conversationIdentifier: bestAttemptContent.threadIdentifier,
|
||||
serviceName: nil,
|
||||
sender: senderPerson,
|
||||
attachments: nil
|
||||
)
|
||||
|
||||
intent.setImage(avatar, forParameterNamed: \.sender)
|
||||
|
||||
let interaction = INInteraction(intent: intent, response: nil)
|
||||
interaction.direction = .incoming
|
||||
|
||||
try? await interaction.donate()
|
||||
|
||||
do {
|
||||
let content = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent
|
||||
return content
|
||||
}
|
||||
catch {}
|
||||
|
||||
return bestAttemptContent
|
||||
}
|
||||
else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
|
||||
func decrypt(ciphertext: String, iv: String? = nil) throws -> [String: Any] {
|
||||
guard var fields = CryptoSettingManager.shared.fields else {
|
||||
throw "No encryption key set"
|
||||
}
|
||||
if let iv = iv {
|
||||
// Support using specified IV parameter for decryption
|
||||
fields.iv = iv
|
||||
}
|
||||
|
||||
let aes = try AESCryptoModel(cryptoFields: fields)
|
||||
|
||||
let json = try aes.decrypt(ciphertext: ciphertext)
|
||||
|
||||
guard let data = json.data(using: .utf8), let map = JSON(data).dictionaryObject else {
|
||||
throw "JSON parsing failed"
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> ()) {
|
||||
guard let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
|
||||
var userInfo = bestAttemptContent.userInfo
|
||||
// 如果是加密推送,则使用密文配置 bestAttemptContent
|
||||
if let ciphertext = userInfo["ciphertext"] as? String {
|
||||
do {
|
||||
var map = try decrypt(ciphertext: ciphertext, iv: userInfo["iv"] as? String)
|
||||
for (key, val) in map {
|
||||
// 将key重写为小写
|
||||
map[key.lowercased()] = val
|
||||
}
|
||||
|
||||
var alert = [String: Any]()
|
||||
if let title = map["title"] as? String {
|
||||
bestAttemptContent.title = title
|
||||
alert["title"] = title
|
||||
}
|
||||
if let body = map["body"] as? String {
|
||||
bestAttemptContent.body = body
|
||||
alert["body"] = body
|
||||
}
|
||||
if let group = map["group"] as? String {
|
||||
bestAttemptContent.threadIdentifier = group
|
||||
}
|
||||
if var sound = map["sound"] as? String {
|
||||
if !sound.hasSuffix(".caf") {
|
||||
sound = "\(sound).caf"
|
||||
}
|
||||
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound))
|
||||
}
|
||||
if let badge = map["badge"] as? Int {
|
||||
bestAttemptContent.badge = badge as NSNumber
|
||||
}
|
||||
|
||||
map["aps"] = ["alert": alert]
|
||||
userInfo = map
|
||||
bestAttemptContent.userInfo = userInfo
|
||||
}
|
||||
catch {
|
||||
bestAttemptContent.body = "Decryption Failed"
|
||||
bestAttemptContent.userInfo = ["aps": ["alert": ["body": bestAttemptContent.body]]]
|
||||
contentHandler(bestAttemptContent)
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
Task {
|
||||
guard var bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
|
||||
contentHandler(request.content)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 通知中断级别
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
if let level = userInfo["level"] as? String {
|
||||
let interruptionLevels: [String: UNNotificationInterruptionLevel] = [
|
||||
"passive": UNNotificationInterruptionLevel.passive,
|
||||
"active": UNNotificationInterruptionLevel.active,
|
||||
"timeSensitive": UNNotificationInterruptionLevel.timeSensitive,
|
||||
"timesensitive": UNNotificationInterruptionLevel.timeSensitive,
|
||||
"critical": UNNotificationInterruptionLevel.critical,
|
||||
]
|
||||
bestAttemptContent.interruptionLevel = interruptionLevels[level] ?? .active
|
||||
|
||||
let processors: [NotificationContentProcessorItem] = [
|
||||
.ciphertext,
|
||||
.level,
|
||||
.badge,
|
||||
.autoCopy,
|
||||
.archive,
|
||||
.setIcon,
|
||||
.setImage
|
||||
]
|
||||
|
||||
for item in processors {
|
||||
do {
|
||||
bestAttemptContent = try await item.processor.process(content: bestAttemptContent)
|
||||
} catch NotificationContentProcessorError.error(let content) {
|
||||
contentHandler(content)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 通知角标
|
||||
if let badgeStr = userInfo["badge"] as? String, let badge = Int(badgeStr) {
|
||||
bestAttemptContent.badge = NSNumber(value: badge)
|
||||
}
|
||||
|
||||
// 自动复制
|
||||
autoCopy(userInfo, defaultCopy: bestAttemptContent.body)
|
||||
|
||||
// 保存推送
|
||||
archive(userInfo)
|
||||
|
||||
Task.init {
|
||||
// 设置推送图标
|
||||
let iconResult = await setIcon(content: bestAttemptContent)
|
||||
// 设置推送图片
|
||||
let imageResult = await self.setImage(content: iconResult)
|
||||
contentHandler(imageResult)
|
||||
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
//
|
||||
// ArchiveProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RealmSwift
|
||||
|
||||
class ArchiveProcessor: NotificationContentProcessor {
|
||||
private lazy var realm: Realm? = {
|
||||
Realm.Configuration.defaultConfiguration = kRealmDefaultConfiguration
|
||||
return try? Realm()
|
||||
}()
|
||||
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
|
||||
var isArchive: Bool = ArchiveSettingManager.shared.isArchive
|
||||
if let archive = userInfo["isarchive"] as? String {
|
||||
isArchive = archive == "1" ? true : false
|
||||
}
|
||||
|
||||
if isArchive {
|
||||
let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any]
|
||||
let title = alert?["title"] as? String
|
||||
let body = alert?["body"] as? String
|
||||
let url = userInfo["url"] as? String
|
||||
let group = userInfo["group"] as? String
|
||||
|
||||
try? realm?.write {
|
||||
let message = Message()
|
||||
message.title = title
|
||||
message.body = body
|
||||
message.url = url
|
||||
message.group = group
|
||||
message.createDate = Date()
|
||||
realm?.add(message)
|
||||
}
|
||||
}
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
//
|
||||
// AutoCopyProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AutoCopyProcessor: NotificationContentProcessor {
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
if userInfo["autocopy"] as? String == "1"
|
||||
|| userInfo["automaticallycopy"] as? String == "1"
|
||||
{
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
} else {
|
||||
UIPasteboard.general.string = bestAttemptContent.body
|
||||
}
|
||||
}
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
19
NotificationServiceExtension/Processor/BadgeProcessor.swift
Normal file
19
NotificationServiceExtension/Processor/BadgeProcessor.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// BadgeProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 通知角标
|
||||
class BadgeProcessor: NotificationContentProcessor {
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
if let badgeStr = bestAttemptContent.userInfo["badge"] as? String, let badge = Int(badgeStr) {
|
||||
bestAttemptContent.badge = NSNumber(value: badge)
|
||||
}
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
//
|
||||
// CiphertextProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftyJSON
|
||||
|
||||
/// 加密推送
|
||||
class CiphertextProcessor: NotificationContentProcessor {
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
var userInfo = bestAttemptContent.userInfo
|
||||
guard let ciphertext = userInfo["ciphertext"] as? String else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
// 如果是加密推送,则使用密文配置 bestAttemptContent
|
||||
do {
|
||||
var map = try decrypt(ciphertext: ciphertext, iv: userInfo["iv"] as? String)
|
||||
|
||||
var alert = [String: Any]()
|
||||
if let title = map["title"] as? String {
|
||||
bestAttemptContent.title = title
|
||||
alert["title"] = title
|
||||
}
|
||||
if let body = map["body"] as? String {
|
||||
bestAttemptContent.body = body
|
||||
alert["body"] = body
|
||||
}
|
||||
if let group = map["group"] as? String {
|
||||
bestAttemptContent.threadIdentifier = group
|
||||
}
|
||||
if var sound = map["sound"] as? String {
|
||||
if !sound.hasSuffix(".caf") {
|
||||
sound = "\(sound).caf"
|
||||
}
|
||||
bestAttemptContent.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: sound))
|
||||
}
|
||||
if let badge = map["badge"] as? Int {
|
||||
bestAttemptContent.badge = badge as NSNumber
|
||||
}
|
||||
|
||||
map["aps"] = ["alert": alert]
|
||||
userInfo = map
|
||||
bestAttemptContent.userInfo = userInfo
|
||||
return bestAttemptContent
|
||||
} catch {
|
||||
bestAttemptContent.body = "Decryption Failed"
|
||||
bestAttemptContent.userInfo = ["aps": ["alert": ["body": bestAttemptContent.body]]]
|
||||
throw NotificationContentProcessorError.error(content: bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
/// 解密文本
|
||||
/// - Parameters:
|
||||
/// - ciphertext: 密文
|
||||
/// - iv: iv 如果不传就用配置保存的,传了就以传的 iv 为准
|
||||
/// - Returns: 解密后的 json 数据
|
||||
private func decrypt(ciphertext: String, iv: String? = nil) throws -> [AnyHashable: Any] {
|
||||
guard var fields = CryptoSettingManager.shared.fields else {
|
||||
throw "No encryption key set"
|
||||
}
|
||||
if let iv = iv {
|
||||
// Support using specified IV parameter for decryption
|
||||
fields.iv = iv
|
||||
}
|
||||
|
||||
let aes = try AESCryptoModel(cryptoFields: fields)
|
||||
|
||||
let json = try aes.decrypt(ciphertext: ciphertext)
|
||||
|
||||
guard let data = json.data(using: .utf8), let map = JSON(data).dictionaryObject else {
|
||||
throw "JSON parsing failed"
|
||||
}
|
||||
|
||||
var result: [AnyHashable: Any] = [:]
|
||||
for (key, val) in map {
|
||||
// 将key重写为小写
|
||||
result[key.lowercased()] = val
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
75
NotificationServiceExtension/Processor/IconProcessor.swift
Normal file
75
NotificationServiceExtension/Processor/IconProcessor.swift
Normal file
@ -0,0 +1,75 @@
|
||||
//
|
||||
// IconProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Intents
|
||||
|
||||
class IconProcessor: NotificationContentProcessor {
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
|
||||
guard let imageUrl = userInfo["icon"] as? String,
|
||||
let imageFileUrl = await ImageDownloader.downloadImage(imageUrl)
|
||||
else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
var personNameComponents = PersonNameComponents()
|
||||
personNameComponents.nickname = bestAttemptContent.title
|
||||
|
||||
let avatar = INImage(imageData: NSData(contentsOfFile: imageFileUrl)! as Data)
|
||||
let senderPerson = INPerson(
|
||||
personHandle: INPersonHandle(value: "", type: .unknown),
|
||||
nameComponents: personNameComponents,
|
||||
displayName: personNameComponents.nickname,
|
||||
image: avatar,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil,
|
||||
isMe: false,
|
||||
suggestionType: .none
|
||||
)
|
||||
let mePerson = INPerson(
|
||||
personHandle: INPersonHandle(value: "", type: .unknown),
|
||||
nameComponents: nil,
|
||||
displayName: nil,
|
||||
image: nil,
|
||||
contactIdentifier: nil,
|
||||
customIdentifier: nil,
|
||||
isMe: true,
|
||||
suggestionType: .none
|
||||
)
|
||||
|
||||
let intent = INSendMessageIntent(
|
||||
recipients: [mePerson],
|
||||
outgoingMessageType: .outgoingMessageText,
|
||||
content: bestAttemptContent.body,
|
||||
speakableGroupName: INSpeakableString(spokenPhrase: personNameComponents.nickname ?? ""),
|
||||
conversationIdentifier: bestAttemptContent.threadIdentifier,
|
||||
serviceName: nil,
|
||||
sender: senderPerson,
|
||||
attachments: nil
|
||||
)
|
||||
|
||||
intent.setImage(avatar, forParameterNamed: \.sender)
|
||||
|
||||
let interaction = INInteraction(intent: intent, response: nil)
|
||||
interaction.direction = .incoming
|
||||
|
||||
do {
|
||||
try await interaction.donate()
|
||||
let content = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent
|
||||
return content
|
||||
} catch {
|
||||
return bestAttemptContent
|
||||
}
|
||||
} else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
}
|
||||
62
NotificationServiceExtension/Processor/ImageDownloader.swift
Normal file
62
NotificationServiceExtension/Processor/ImageDownloader.swift
Normal file
@ -0,0 +1,62 @@
|
||||
//
|
||||
// ImageDownloader.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Kingfisher
|
||||
|
||||
class ImageDownloader {
|
||||
/// 保存图片到缓存中
|
||||
/// - Parameters:
|
||||
/// - cache: 使用的缓存
|
||||
/// - data: 图片 Data 数据
|
||||
/// - key: 缓存 Key
|
||||
class func storeImage(cache: ImageCache, data: Data, key: String) async {
|
||||
return await withCheckedContinuation { continuation in
|
||||
cache.storeToDisk(data, forKey: key, expiration: StorageExpiration.never) { _ in
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 使用 Kingfisher.ImageDownloader 下载图片
|
||||
/// - Parameter url: 下载的图片URL
|
||||
/// - Returns: 返回 Result
|
||||
class func downloadImage(url: URL) async -> Result<ImageLoadingResult, KingfisherError> {
|
||||
return await withCheckedContinuation { continuation in
|
||||
Kingfisher.ImageDownloader.default.downloadImage(with: url, options: nil) { result in
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载推送图片
|
||||
/// - Parameter imageUrl: 图片URL字符串
|
||||
/// - Returns: 保存在本地中的`图片 File URL`
|
||||
class func downloadImage(_ imageUrl: String) async -> String? {
|
||||
guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark"),
|
||||
let cache = try? ImageCache(name: "shared", cacheDirectoryURL: groupUrl),
|
||||
let imageResource = URL(string: imageUrl)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 先查看图片缓存
|
||||
if cache.diskStorage.isCached(forKey: imageResource.cacheKey) {
|
||||
return cache.cachePath(forKey: imageResource.cacheKey)
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
guard let result = try? await downloadImage(url: imageResource).get() else {
|
||||
return nil
|
||||
}
|
||||
// 缓存图片
|
||||
await storeImage(cache: cache, data: result.originalData, key: imageResource.cacheKey)
|
||||
|
||||
return cache.cachePath(forKey: imageResource.cacheKey)
|
||||
}
|
||||
}
|
||||
37
NotificationServiceExtension/Processor/ImageProcessor.swift
Normal file
37
NotificationServiceExtension/Processor/ImageProcessor.swift
Normal file
@ -0,0 +1,37 @@
|
||||
//
|
||||
// ImageProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
|
||||
class ImageProcessor: NotificationContentProcessor {
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
guard let imageUrl = userInfo["image"] as? String,
|
||||
let imageFileUrl = await ImageDownloader.downloadImage(imageUrl)
|
||||
else {
|
||||
return bestAttemptContent
|
||||
}
|
||||
|
||||
let copyDestUrl = URL(fileURLWithPath: imageFileUrl).appendingPathExtension(".tmp")
|
||||
// 将图片缓存复制一份,推送使用完后会自动删除,但图片缓存需要留着以后在历史记录里查看
|
||||
try? FileManager.default.copyItem(
|
||||
at: URL(fileURLWithPath: imageFileUrl),
|
||||
to: copyDestUrl
|
||||
)
|
||||
|
||||
if let attachment = try? UNNotificationAttachment(
|
||||
identifier: "image",
|
||||
url: copyDestUrl,
|
||||
options: [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]
|
||||
) {
|
||||
bestAttemptContent.attachments = [attachment]
|
||||
}
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
28
NotificationServiceExtension/Processor/LevelProcessor.swift
Normal file
28
NotificationServiceExtension/Processor/LevelProcessor.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// LevelProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// 通知中断级别
|
||||
class LevelProcessor: NotificationContentProcessor {
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent {
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
if let level = bestAttemptContent.userInfo["level"] as? String {
|
||||
let interruptionLevels: [String: UNNotificationInterruptionLevel] = [
|
||||
"passive": UNNotificationInterruptionLevel.passive,
|
||||
"active": UNNotificationInterruptionLevel.active,
|
||||
"timeSensitive": UNNotificationInterruptionLevel.timeSensitive,
|
||||
"timesensitive": UNNotificationInterruptionLevel.timeSensitive,
|
||||
"critical": UNNotificationInterruptionLevel.critical
|
||||
]
|
||||
bestAttemptContent.interruptionLevel = interruptionLevels[level] ?? .active
|
||||
}
|
||||
}
|
||||
return bestAttemptContent
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
//
|
||||
// NotificationContentProcessor.swift
|
||||
// NotificationServiceExtension
|
||||
//
|
||||
// Created by huangfeng on 2024/5/29.
|
||||
// Copyright © 2024 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@_exported import UserNotifications
|
||||
|
||||
enum NotificationContentProcessorItem {
|
||||
case ciphertext
|
||||
case level
|
||||
case badge
|
||||
case autoCopy
|
||||
case archive
|
||||
case setIcon
|
||||
case setImage
|
||||
|
||||
var processor: NotificationContentProcessor {
|
||||
switch self {
|
||||
case .ciphertext:
|
||||
return CiphertextProcessor()
|
||||
case .level:
|
||||
return LevelProcessor()
|
||||
case .badge:
|
||||
return BadgeProcessor()
|
||||
case .autoCopy:
|
||||
return AutoCopyProcessor()
|
||||
case .archive:
|
||||
return ArchiveProcessor()
|
||||
case .setIcon:
|
||||
return IconProcessor()
|
||||
case .setImage:
|
||||
return ImageProcessor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NotificationContentProcessorError: Swift.Error {
|
||||
case error(content: UNMutableNotificationContent)
|
||||
}
|
||||
|
||||
public protocol NotificationContentProcessor {
|
||||
/// 处理 UNMutableNotificationContent
|
||||
/// - Parameter bestAttemptContent: 需要处理的 UNMutableNotificationContent
|
||||
/// - Returns: 处理成功后的 UNMutableNotificationContent
|
||||
/// - Throws: 处理失败后,应该中断处理
|
||||
func process(content bestAttemptContent: UNMutableNotificationContent) async throws -> UNMutableNotificationContent
|
||||
}
|
||||
@ -153,4 +153,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 73cfda85b7a345d896330d72bc61822356f98586
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.15.2
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user