iOS 微信语音电话推送响铃实现

一、目标功能

  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文件夹

swift 复制代码
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方法中调用删除通知的方法后,方法还没有执行完成,推送扩展进程就已经挂了(真坑啊,当时一度怀疑方法没调对)

swift 复制代码
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.推送超时处理

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

5.应用激活停止响铃

由于主应用与扩展属于两个进程,苹果的沙盒机制使这两个进程不能直接的进行常规通信,而AppGroup可以开辟一块可以共享的内存,让两个进程进行数据的读取,从而达到通信的目的(AppGroup的创建请自行查阅)。需要注意的是主程序、扩展程序target都需要创建AppGroup,且字段名相同

swift 复制代码
// 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)
    }
}

结语

至此也就基本完整的实现了语音电话推送响铃需求,由于之前我没有接触过扩展开发,在功能研究过程中坑是一个接一个的踩,过程比较辛酸,如果本篇对你有帮助或触动的话,顺手一个小赞鼓励一下。

相关推荐
叽哥3 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户091 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan1 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918412 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张2 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa
00后程序员张3 天前
详细解析苹果iOS应用上架到App Store的完整步骤与指南
android·ios·小程序·https·uni-app·iphone·webview