寫這個需求踩了很多坑,記憶深刻了,必須要記錄一下了......
push帶圖片的樣式:
push小圖.PNG
|
push小圖長按詳情.PNG
|
---|
創建 Notification Service Extension
-
選中File->New->Target,選中NotificationServiceExtension
image.png
(坑一 Xcode bug: 我選中File->New->Target,就崩潰,80%概率崩潰,我也挺崩潰的Xcode版本12.0.1)
-
需要配置NotificationServiceExtension target的Bundle ID,Profile文件(需要在apple開發者中心配置)。注意team和sign和主target保持一致。
image.png -
創建
extension
之后會自動創建一個NotificationService
文件。注意最好不要自己去修改它。(坑二自己作: 我自己最開始創建的時候是OC,后來被建議換成Swift文件,我就直接把OC文件給刪除了,但是Swift代碼并沒有生效,應該是系統沒有識別出這個文件,后來又刪掉extension
重新創建的,還是不要瞎折騰的好,折騰的話需要好好研究info.plist
里面的NSExtensionPrincipalClass
,猜測。這里我直接用暴力刪除重建的方式解決了,不過感興趣的可以研究)
image.png 代碼,解析的時候注意自己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,下載下來是這樣的。
image.png 注冊
didRegisterForRemoteNotificationsWithDeviceToken
回調里面拿到push token。安裝一個dev環境下的推送證書(test測試環境),然后在這個工具里選擇這個證書。
數據都填好后,app回到后臺,點擊push即可看到效果。
調試 Notification Service Extension
- 直接運行主app,在extension里面打的斷點是不會走的。
-
需要選中extension taget,然后點擊運行,在彈出的框中選擇主app,點擊run運行起來。
image.png
- 在
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。
image.png - 下載小圖保存的沙盒地址是這樣的(驗證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
打印地址,不同的通知地址不一致。
image.png - extension的target的支持的iOS的系統和主target保持一致,以免出現部分手機收不到小圖push問題
天坑:同事review代碼時想看下我的需求,結果他手機沒顯示小圖(他手機iOS14.3, iPhone X),懷疑我代碼有問題。我把我手機升級和他一樣的系統,測試沒問題,又試了好幾個別的手機都沒問題,到處查資料,搜索了一天無果。 后來隨機提到重啟手機過沒有,因為不知為啥他手機升級過后系統bug很多,結果重啟完再push,他收到圖片push啦。想哭......還是重啟大法好啊......
天坑:又一手機,莫名其妙didRegisterForRemoteNotificationsWithDeviceToken
和didFailToRegisterForRemoteNotificationsWithError
不調用。那么看看這里
重點是:1. 關機重啟 2.或wifi bug,插卡 3.或關機插卡
在你崩潰之前,記得重啟手機,說不定很多問題壓根不用解決。
不過經歷上件事情,如果沒有重啟,我還是定位不到原因(因為所有的條件都滿足,沒有原因呀),這種情況下,要如何解決問題,不被block需求值得思考,歡迎討論和指導。
參考: