iOS rich push 远程通知带图片NotificationServiceExtension

写这个需求踩了很多坑,记忆深刻了,必须要记录一下了......

push带图片的样式:

push小图.PNG
push小图长按详情.PNG

创建 Notification Service Extension

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


    image.png

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

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


    image.png
  3. 创建extension之后会自动创建一个NotificationService文件。注意最好不要自己去修改它。(坑二自己作: 我自己最开始创建的时候是OC,后来被建议换成Swift文件,我就直接把OC文件给删除了,但是Swift代码并没有生效,应该是系统没有识别出这个文件,后来又删掉extension重新创建的,还是不要瞎折腾的好,折腾的话需要好好研究info.plist里面的NSExtensionPrincipalClass,猜测。这里我直接用暴力删除重建的方式解决了,不过感兴趣的可以研究)

    image.png

  4. 代码,解析的时候注意自己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()
    }
}

  1. 测试的样例,和上面的代码层次匹配。注意必须设置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

  1. 这个Mac工具NWPusher还挺好用的(宝藏),可以发送push,不用别人配合,通知立马就到。按照链接里面去下载这个mac工具 https://github.com/noodlewerk/NWPusher,下载下来是这样的。

    image.png

  2. 注册didRegisterForRemoteNotificationsWithDeviceToken回调里面拿到push token。

  3. 安装一个dev环境下的推送证书(test测试环境),然后在这个工具里选择这个证书。

  4. 数据都填好后,app回到后台,点击push即可看到效果。

调试 Notification Service Extension

  1. 直接运行主app,在extension里面打的断点是不会走的。
  • 需要选中extension taget,然后点击运行,在弹出的框中选择主app,点击run运行起来。


    image.png
image.png
  • NotificationService 打上断点
  • app退到后台,用NWPusher工具发送一个图片的payload。
  • 收到通知时会进入断点
  1. 开始以为不能调试,也不进断点,直接在
    contentHandler(bestAttemptContent) 前修改bestAttemptContent.title,看我修改的push title是否生效了来测试哪一步出现了问题。

注意点⚠️

  1. 必须开通通知权限
  2. 发送的payload必须包含"mutable-content": 1才能进入extesnion
  3. 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.

  1. 下发的图片链接默认只支持https,若要支持http需要修改extension中的info.plist。
    image.png
  2. 下载小图保存的沙盒地址是这样的(验证app extension和主app是隔离开的,不是同一个沙盒哦),file:///var/mobile/Containers/Data/PluginKitPlugin/EEF3E755-E79B-4C7F-A83F-F20642C805C3/Library/Caches/。write的图片在push成功后会被系统删掉,所以不需要管理文件过多的问题。
  3. pushExtension 是否能访问主target的文件:可以
  • 将需要访问的那个文件,在extension的target上也打上勾勾


    打上勾勾
  • 如果需要在extension中访问pod,那么也需要在extension target中pod进入,然后在NotificationService.swfit文件中import
  1. 发送多条通知时,NotificationService会创建几个实例,还是共用一个:会创建多个,验证过在NotificationService打印地址,不同的通知地址不一致。
    image.png
  2. extension的target的支持的iOS的系统和主target保持一致,以免出现部分手机收不到小图push问题

天坑:同事review代码时想看下我的需求,结果他手机没显示小图(他手机iOS14.3, iPhone X),怀疑我代码有问题。我把我手机升级和他一样的系统,测试没问题,又试了好几个别的手机都没问题,到处查资料,搜索了一天无果。 后来随机提到重启手机过没有,因为不知为啥他手机升级过后系统bug很多,结果重启完再push,他收到图片push啦。想哭......还是重启大法好啊......

天坑:又一手机,莫名其妙didRegisterForRemoteNotificationsWithDeviceTokendidFailToRegisterForRemoteNotificationsWithError不调用。那么看看这里
重点是:1. 关机重启 2.或wifi bug,插卡 3.或关机插卡

在你崩溃之前,记得重启手机,说不定很多问题压根不用解决。

不过经历上件事情,如果没有重启,我还是定位不到原因(因为所有的条件都满足,没有原因呀),这种情况下,要如何解决问题,不被block需求值得思考,欢迎讨论和指导。

参考:

  1. https://onevcat.com/2016/08/notification/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容