一、方针功用

  1. 设备收到App语音电话推送音讯,设备继续轰动、响铃
  2. 响铃期间告诉栏显现推送音讯内容
  3. 点击使用图标或告诉栏使用进入前台,中止响铃
  4. 超时未响应:中止响铃;告诉内容显现“未接听通话”

二、功用难点

  1. 苹果APNS推送虽然能指定推送声音文件等功用,却并不能继续的轰动、响铃;而Notification Service Extension能够让你对收到的APNS推送有30s的时间进行处理。
  2. 使用Extension处理推送完结前,推送栏没有内容;此刻能够发送一条本地推送显现:“收到语音电话”;这样又出现了新的问题:Extension超时会将长途推送显现到告诉栏,导致告诉栏显现两条推送音讯。
  3. 监听到使用进入前台激活时需求中止Extension的响铃,而Extension与主使用并不在同一个进程,想要通讯的话需求凭借AppGroup功用

三、完成功用

1.Notification Service Extension

创立Extension的步骤不在此篇进行详细解说,有疑问的话能够具体查一查。Extension创立好后,项目中会多出一个文件夹,文件夹名为扩展创立时的名字(后续称呼为Notification),文件夹下有一个NotificationService文件,里面现已有一个NotificationService类和两个办法

didReceiveNotificationRequest:withContentHandler:收到推送音讯触发

serviceExtensionTimeWillExpire:推送音讯处理超时触发

运行Extension:
切换项目Target,Run一下,然后挑选主使用即可;因为我是Flutter写的使用,Debug模式下热更新的原因挑选主使用Run的时候并不能跑起来,此刻只需求修改主使用和Extension的scheme重新Run

2.继续轰动、响铃

需求提前将音频文件的引证拖入Notification文件夹

var soundID: SystemSoundID = 0
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void {
    ...
    startAudioWork()
    ...
}
// 开端播映
private func startAudioWork() {
    let audioPath = Bundle.main.path(forResource: "音频文件名", ofType: "mp3")
    let fileUrl = URL(string: audioPath ?? "")
    // 创立响铃任务
    AudioServicesCreateSystemSoundID(fileUrl! as CFURL, &soundID)
    // 播映轰动、响铃
    AudioServicesPlayAlertSound(soundID)
    // 监听响铃完结状态
    AudioServicesAddSystemSoundCompletion(soundID, nil, nil, {sound, clientData in
        // 音频文件一次播映完结,再次播映
        AudioServicesPlayAlertSound(sound)
    }, nil)
}
// 中止播映
private func stopAudioWork() {
    AudioServicesRemoveSystemSoundCompletion(soundID)
    AudioServicesDisposeSystemSoundID(soundID)
}

3.响铃时告诉栏显现内容

因为在NotificationServiceExtension处理完结前,表示该告诉还在处理,故:此刻告诉栏不会有该条推送的内容;那么在响铃的同时就要显现推送内容的话,咱们只好手动加一条本地告诉,为了防止推送处理超时告诉栏同时存在一条本地推送和一条长途推送,咱们需求将本地推送的ID设置成长途推送的ID,开端我打算在超时回调处直接删去本地推送,很可惜实践并不能成功,经查阅苹果文档删去本地推送的办法是异步的,在serviceExtensionTimeWillExpire办法中调用删去告诉的办法后,办法还没有执行完结,推送扩展进程就现已挂了(真坑啊,当时一度置疑办法没调对)

override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void {
    self.contentHandler = contentHandler
    self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
    startAudioWork()
    sendLocalNotification(identifier: request.identifier, body: bestAttemptContent?.body)
    ...
}
// 本地推送
private func sendLocalNotification(identifier: String, body: String?) {
    // 推送id和推送的内容都使用长途APNS的
    let content = UNMutableNotificationContent()
    content.body = body ?? ""
    let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
    UNUserNotificationCenter.current().add(request)
}

4.推送超时处理

override func serviceExtensionTimeWillExpire() {
    stopAudioWork()
    if let handler = self.contentHandler, let content = self.bestAttemptContent {
        content.body = "[未接听通话]"
        // 推送处理完结
        handler(content)
    }
}

5.使用激活中止响铃

因为主使用与扩展属于两个进程,苹果的沙盒机制使这两个进程不能直接的进行惯例通讯,而AppGroup能够开辟一块能够同享的内存,让两个进程进行数据的读取,然后到达通讯的目的(AppGroup的创立请自行查阅)。需求留意的是主程序、扩展程序target都需求创立AppGroup,且字段名相同

// AppDelegate 使用进入前台
func applicationDidBecomeActive(_ application: UIApplication) {
    // 通过AppGroupID创立UserDefaults
    let userDefaults = UserDefaults(suiteName: "group.bundleID")
    // 更新AppGroup数据(1:中止响铃)
    userDefaults?.set(1, forKey: "VoiceKey")
    // 移除告诉栏音讯
    UNUserNotificationCenter.current().removeAllDeliveredNotifications()
}
var soundID: SystemSoundID = 0
let appGroup = "group.boundleID"
let key = "VoiceKey"
class NotificationService: UNNotificationServiceExtension {
    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    let userDefaults = UserDefaults(suiteName: appGroup)
    // 告诉扩展收到推送音讯
    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void {
        self.contentHandler = contentHandler
        self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        // 播映轰动、响铃
        startAudioWork()
        // 发送本地告诉
        sendLocalNotification(identifier: request.identifier, body: bestAttemptContent?.body)
        // 更新AppGroup正在响铃
        userDefaults?.set(0, forKey: key)
    }
    // 开端播映
    private func startAudioWork() {
        let audioPath = Bundle.main.path(forResource: "音频文件名", ofType: "mp3")
        let fileUrl = URL(string: audioPath ?? "")
        // 创立响铃任务
        AudioServicesCreateSystemSoundID(fileUrl! as CFURL, &soundID)
        // 播映轰动、响铃
        AudioServicesPlayAlertSound(soundID)
        // 监听响铃完结状态
        let selfPointer = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
        AudioServicesAddSystemSoundCompletion(soundID, nil, nil, {sound, clientData in
            guard let pointer = clientData else { return }
            let selfP = unsafeBitCast(pointer, NotificationService.self)
            let value = selfP.userDefaults?.integer(forKey: key) ?? 0
            if value == 1 {
                // app进入前台,中止响铃
                selfP.stopAudioWork()
                // 推送处理完毕
                if let handler = selfP.contentHandler, let content = selfP.bestAttemptContent {
                    handler(content)
                }
            } else {
                AudioServicesPlayAlertSound(sound)
            }
        }, selfPointer)
    }
}

结语

至此也就基本完好的完成了语音电话推送响铃需求,因为之前我没有触摸过扩展开发,在功用研讨进程中坑是一个接一个的踩,进程比较痛苦,如果本篇对你有帮助或触动的话,顺手一个小赞鼓舞一下。