iOS 遠(yuǎn)程通知帶圖片NotificationServiceExtension(rich push)

寫這個(gè)需求踩了很多坑,記憶深刻了,必須要記錄一下了......

push帶圖片的樣式:

|

image

|

image

|
| --- | --- |

創(chuàng)建 Notification Service Extension

  1. 選中File->New->Target,選中NotificationServiceExtension

    image

    (坑一 Xcode bug: 我選中File->New->Target,就崩潰,80%概率崩潰,我也挺崩潰的Xcode版本12.0.1)

  2. 需要配置NotificationServiceExtension target的Bundle ID,Profile文件(需要在apple開發(fā)者中心配置)。注意team和sign和主target保持一致。

    image
  3. 創(chuàng)建extension之后會(huì)自動(dòng)創(chuàng)建一個(gè)NotificationService文件。注意最好不要自己去修改它。(坑二自己作: 我自己最開始創(chuàng)建的時(shí)候是OC,后來被建議換成Swift文件,我就直接把OC文件給刪除了,但是Swift代碼并沒有生效,應(yīng)該是系統(tǒng)沒有識(shí)別出這個(gè)文件,后來又刪掉extension重新創(chuàng)建的,還是不要瞎折騰的好,折騰的話需要好好研究info.plist里面的NSExtensionPrincipalClass,猜測(cè)。這里我直接用暴力刪除重建的方式解決了,不過感興趣的可以研究)

    image
  4. 代碼,解析的時(shí)候注意自己url的字典層次結(jié)構(gòu),自行修改,這里的代碼和下面我發(fā)的樣例匹配

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
        }
        //此處回傳一個(gè)description,是為了方便調(diào)試發(fā)生錯(cuò)誤的點(diǎn)在哪,通過修改bestAttemptContent.title = description。不過后來我找到了能走到斷點(diǎn)的方式了
        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()
    }
}

  1. 測(cè)試的樣例,和上面的代碼層次匹配。注意必須設(shè)置mutable-content : 1才會(huì)走到extension里面來,當(dāng)然也要注意開啟了允許通知的權(quán)限。
{
  "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/"
}

測(cè)試帶圖片的push

  1. 這個(gè)Mac工具NWPusher還挺好用的(寶藏),可以發(fā)送push,不用別人配合,通知立馬就到。按照鏈接里面去下載這個(gè)mac工具 https://github.com/noodlewerk/NWPusher,下載下來是這樣的。

    image
  2. 注冊(cè)didRegisterForRemoteNotificationsWithDeviceToken回調(diào)里面拿到push token。

  3. 安裝一個(gè)dev環(huán)境下的推送證書(test測(cè)試環(huán)境),然后在這個(gè)工具里選擇這個(gè)證書。

  4. 數(shù)據(jù)都填好后,app回到后臺(tái),點(diǎn)擊push即可看到效果。

調(diào)試 Notification Service Extension

  1. 直接運(yùn)行主app,在extension里面打的斷點(diǎn)是不會(huì)走的。
  • 需要選中extension taget,然后點(diǎn)擊運(yùn)行,在彈出的框中選擇主app,點(diǎn)擊run運(yùn)行起來。

    image
image
  • NotificationService 打上斷點(diǎn)
  • app退到后臺(tái),用NWPusher工具發(fā)送一個(gè)圖片的payload。
  • 收到通知時(shí)會(huì)進(jìn)入斷點(diǎn)
  1. 開始以為不能調(diào)試,也不進(jìn)斷點(diǎn),直接在
    contentHandler(bestAttemptContent) 前修改bestAttemptContent.title,看我修改的push title是否生效了來測(cè)試哪一步出現(xiàn)了問題。

注意點(diǎn)??

  1. 必須開通通知權(quán)限
  2. 發(fā)送的payload必須包含"mutable-content": 1才能進(jìn)入extesnion
  3. code sign和team要注意和主target保持一致,否則報(bào)以下錯(cuò)。

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.

  1. 下發(fā)的圖片鏈接默認(rèn)只支持https,若要支持http需要修改extension中的info.plist。

    image
  2. 下載小圖保存的沙盒地址是這樣的(驗(yàn)證app extension和主app是隔離開的,不是同一個(gè)沙盒哦),file:///var/mobile/Containers/Data/PluginKitPlugin/EEF3E755-E79B-4C7F-A83F-F20642C805C3/Library/Caches/。write的圖片在push成功后會(huì)被系統(tǒng)刪掉,所以不需要管理文件過多的問題。

  3. pushExtension 是否能訪問主target的文件:可以

  • 將需要訪問的那個(gè)文件,在extension的target上也打上勾勾

    image
  • 如果需要在extension中訪問pod,那么也需要在extension target中pod進(jìn)入,然后在NotificationService.swfit文件中import。

  1. 發(fā)送多條通知時(shí),NotificationService會(huì)創(chuàng)建幾個(gè)實(shí)例,還是共用一個(gè):會(huì)創(chuàng)建多個(gè),驗(yàn)證過在NotificationService打印地址,不同的通知地址不一致。

    image
  2. extension的target的支持的iOS的系統(tǒng)和主target保持一致,以免出現(xiàn)部分手機(jī)收不到小圖push問題

天坑:同事review代碼時(shí)想看下我的需求,結(jié)果他手機(jī)沒顯示小圖(他手機(jī)iOS14.3, iPhone X),懷疑我代碼有問題。我把我手機(jī)升級(jí)和他一樣的系統(tǒng),測(cè)試沒問題,又試了好幾個(gè)別的手機(jī)都沒問題,到處查資料,搜索了一天無果。 后來隨機(jī)提到重啟手機(jī)過沒有,因?yàn)椴恢獮樯端謾C(jī)升級(jí)過后系統(tǒng)bug很多,結(jié)果重啟完再push,他收到圖片push啦。想哭......還是重啟大法好啊......

天坑:又一手機(jī),莫名其妙didRegisterForRemoteNotificationsWithDeviceTokendidFailToRegisterForRemoteNotificationsWithError不調(diào)用。那么看看這里
重點(diǎn)是:1. 關(guān)機(jī)重啟 2.或wifi bug,插卡 3.或關(guān)機(jī)插卡

在你崩潰之前,記得重啟手機(jī),說不定很多問題壓根不用解決。

不過經(jīng)歷上件事情,如果沒有重啟,我還是定位不到原因(因?yàn)樗械臈l件都滿足,沒有原因呀),這種情況下,要如何解決問題,不被block需求值得思考,歡迎討論和指導(dǎo)。

參考:

  1. https://onevcat.com/2016/08/notification/

作者:落夏簡(jiǎn)葉
鏈接:http://www.itdecent.cn/p/30ac89ab7797
來源:簡(jiǎn)書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容