写这个需求踩了很多坑,记忆深刻了,必须要记录一下了......
push带图片的样式:
创建 Notification Service Extension
-
选中File->New->Target,选中NotificationServiceExtension
(坑一 Xcode bug: 我选中File->New->Target,就崩溃,80%概率崩溃,我也挺崩溃的Xcode版本12.0.1)
-
需要配置NotificationServiceExtension target的Bundle ID,Profile文件(需要在apple开发者中心配置)。注意team和sign和主target保持一致。
-
创建
extension
之后会自动创建一个NotificationService
文件。注意最好不要自己去修改它。(坑二自己作: 我自己最开始创建的时候是OC,后来被建议换成Swift文件,我就直接把OC文件给删除了,但是Swift代码并没有生效,应该是系统没有识别出这个文件,后来又删掉extension
重新创建的,还是不要瞎折腾的好,折腾的话需要好好研究info.plist
里面的NSExtensionPrincipalClass
,猜测。这里我直接用暴力删除重建的方式解决了,不过感兴趣的可以研究)
代码,解析的时候注意自己url的字典层次结构,自行修改,这里的代码和下面我发的样例匹配
import UserNotifications
import CommonCrypto
class NotificationService: UNNotificationServiceExtension {
static let notificationServiceImageAttachmentIdentifier = "com.notificationservice.imagedownloaded"
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
let imagekey = "smallImage"
let dataKey = "data"
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
guard let bestAttemptContent = bestAttemptContent else {
return
}
//download image
let userInfo = request.content.userInfo
guard let data = userInfo[dataKey] as? [String: Any],
let image = data[imagekey] as? String, !image.isEmpty,
let imageURL = URL(string: image) else {
contentHandler(bestAttemptContent)
return
}
//此处回传一个description,是为了方便调试发生错误的点在哪,通过修改bestAttemptContent.title = description。不过后来我找到了能走到断点的方式了
downloadAndSave(url: imageURL) { (localURL, description) in
guard let localURL = localURL, let attachment = try? UNNotificationAttachment(identifier: NotificationService.notificationServiceImageAttachmentIdentifier, url: localURL, options: nil) else {
contentHandler(bestAttemptContent)
return
}
bestAttemptContent.attachments = [attachment]
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)
}
}
private func downloadAndSave(url: URL, handler: @escaping (_ localURL: URL?, _ des: String) -> Void) {
let task = URLSession.shared.dataTask(with: url) { (data, res, error) in
var localURL: URL? = nil
guard let data = data else {
handler(nil, "data is null")
return
}
let ext = (url.absoluteString as NSString).pathExtension
let cacheURL = FileManager.cacheDir()
let url = cacheURL.appendingPathComponent(url.absoluteString.md5).appendingPathExtension(ext)
guard let _ = try? data.write(to: url) else {
handler(nil, "data write error")
return
}
localURL = url
handler(localURL, "success")
}
task.resume()
}
}
extension FileManager {
class func cacheDir() -> URL {
let dirPaths = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)
let cacheDir = dirPaths[0] as String
return URL(fileURLWithPath: cacheDir)
}
}
extension String {
var md5: String {
let data = Data(self.utf8)
let hash = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [UInt8] in
var hash = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
CC_MD5(bytes.baseAddress, CC_LONG(data.count), &hash)
return hash
}
return hash.map { String(format: "%02x", $0) }.joined()
}
}
- 测试的样例,和上面的代码层次匹配。注意必须设置
mutable-content : 1
才会走到extension里面来,当然也要注意开启了允许通知的权限。
{
"data": {
"smallImage": "https://onevcat.com/assets/images/background-cover.jpg",
},
"aps": {
"badge": 6,
"alert": {
"subtitle": "sub title",
"body": "Hello Moto!",
"title": "Hi i ii i I I"
},
"sound": "default",
"mutable-content": 1
},
"uri": "https://www.baidu.com/"
}
测试带图片的push
-
这个Mac工具
NWPusher
还挺好用的(宝藏),可以发送push,不用别人配合,通知立马就到。按照链接里面去下载这个mac工具 https://github.com/noodlewerk/NWPusher,下载下来是这样的。
注册
didRegisterForRemoteNotificationsWithDeviceToken
回调里面拿到push token。安装一个dev环境下的推送证书(test测试环境),然后在这个工具里选择这个证书。
数据都填好后,app回到后台,点击push即可看到效果。
调试 Notification Service Extension
- 直接运行主app,在extension里面打的断点是不会走的。
-
需要选中extension taget,然后点击运行,在弹出的框中选择主app,点击run运行起来。
- 在
NotificationService
打上断点 - app退到后台,用
NWPusher
工具发送一个图片的payload。 - 收到通知时会进入断点
- 开始以为不能调试,也不进断点,直接在
contentHandler(bestAttemptContent)
前修改bestAttemptContent.title
,看我修改的push title是否生效了来测试哪一步出现了问题。
注意点⚠️
- 必须开通通知权限
- 发送的
payload
必须包含"mutable-content": 1
才能进入extesnion - code sign和team要注意和主target保持一致,否则报以下错。
Embedded binary is not signed with the same certificate as the parent app. Verify the embedded binary target's code sign settings match the parent app's.
- 下发的图片链接默认只支持
https
,若要支持http
需要修改extension中的info.plist。
- 下载小图保存的沙盒地址是这样的(验证app extension和主app是隔离开的,不是同一个沙盒哦),
file:///var/mobile/Containers/Data/PluginKitPlugin/EEF3E755-E79B-4C7F-A83F-F20642C805C3/Library/Caches/
。write的图片在push成功后会被系统删掉,所以不需要管理文件过多的问题。 - pushExtension 是否能访问主target的文件:可以
-
将需要访问的那个文件,在extension的target上也打上勾勾
- 如果需要在extension中访问pod,那么也需要在extension target中pod进入,然后在
NotificationService.swfit
文件中import
。
- 发送多条通知时,
NotificationService
会创建几个实例,还是共用一个:会创建多个,验证过在NotificationService
打印地址,不同的通知地址不一致。
- extension的target的支持的iOS的系统和主target保持一致,以免出现部分手机收不到小图push问题
天坑:同事review代码时想看下我的需求,结果他手机没显示小图(他手机iOS14.3, iPhone X),怀疑我代码有问题。我把我手机升级和他一样的系统,测试没问题,又试了好几个别的手机都没问题,到处查资料,搜索了一天无果。 后来随机提到重启手机过没有,因为不知为啥他手机升级过后系统bug很多,结果重启完再push,他收到图片push啦。想哭......还是重启大法好啊......
天坑:又一手机,莫名其妙didRegisterForRemoteNotificationsWithDeviceToken
和didFailToRegisterForRemoteNotificationsWithError
不调用。那么看看这里
重点是:1. 关机重启 2.或wifi bug,插卡 3.或关机插卡
在你崩溃之前,记得重启手机,说不定很多问题压根不用解决。
不过经历上件事情,如果没有重启,我还是定位不到原因(因为所有的条件都满足,没有原因呀),这种情况下,要如何解决问题,不被block需求值得思考,欢迎讨论和指导。
参考: