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/

你可能感兴趣的:(iOS rich push 远程通知带图片NotificationServiceExtension)