IOS-Swiftui后台通话响铃震动

IOS后台响铃,通过UNNotificationServiceExtension实现方案

UNNotificationServiceExtension介绍

UNNotificationServiceExtension是iOS中的一种扩展类型,用于处理远程通知的内容和资源。它可以在收到通知后对通知进行修改、添加自定义内容,以及下载和管理相关资源。

UNNotificationServiceExtension的主要作用是提供一个后台执行的环境,用于对通知进行处理和修改。它可以通过实现UNNotificationServiceExtension类来创建一个扩展,然后在Info.plist文件中将其配置为通知的服务扩展。

使用UNNotificationServiceExtension可以提供以下功能:

  1. 修改通知内容:可以在收到通知时修改通知的标题、子标题、正文或其他相关信息。这样可以根据应用的需求对通知内容进行个性化定制。

  2. 添加自定义内容:可以添加自定义的图像、声音或其他资源到通知中,以提供更多的交互和信息展示方式。

  3. 下载和管理资源:可以通过扩展来下载和管理通知中需要使用的资源,例如图片、音频文件等。这样可以避免在主应用中进行资源下载的延迟,并提供更好的用户体验。

  4. 执行后台任务:UNNotificationServiceExtension运行在一个后台进程中,因此可以执行一些耗时的操作,如网络请求、数据处理等。这可以避免在主应用中执行这些任务而影响用户界面的流畅性。

总之,UNNotificationServiceExtension提供了一种灵活、可定制的方式来处理和修改远程通知。它可以帮助开发者提升通知的交互性和用户体验,并提供更多的个性化定制选项。

  1. Xcode创建UNNotificationServiceExtension target
  2. 通过NotificationService的didReceive方法处理指定的通知内容
  3. 使用UserDefaults(suiteName: self.groupName)做共享数据传递

创建UNNotificationServiceExtension target
在这里插入图片描述

点击Xcode-File-New-Target,弹出如下选项,进行过滤选择
在这里插入图片描述
这里我们选择Notification Service Extension
在这里插入图片描述
到这里点击Finish就创建完成了,Xcode将会自动生成必要的代码
在这里插入图片描述
NotificationService就是用来拦截通知的,收到通知时,将会触发didReceive方法

调试判断通知扩展服务生效,只需要看弹出的通知是否增加了"[modified]"内容

实现思路

通话请求是一种通知类型,挂断是另外一种通知类型。在收到通话请求时,会有至多30秒钟时间可以拉活应用,我们需要在这段时间内持续响铃+震动。

开始响铃
收到音视频通话请求通知时,检查APP是否处在前台,处在前台时,由主程序自己实现响铃震动,这里只处理APP不在前台的情况,进行响铃和震动
停止响铃
30秒钟处理时间到了之后,系统将会主动调用serviceExtensionTimeWillExpire超时 方法,我们也可以在超时之前自己主动去停止响铃震动。在这里,每秒检查一次通知是否应该被激活,如果用户打开了主程序,需要在主程序中,将共享内存中的callKeyEnterToForeground参数修改为true,如此,NotificationService将能够检查到用户APP被打开,而停止自己的响铃震动,之后是否需要响铃震动,由主程序自己处理。
当收到停止响铃的通知时,将keyStartCallTime重置为0,如此在震动轮循检查状态时,也能够检查到已经收到了挂断通知,从而终止响铃震动逻辑

以下是完整代码

UNNotificationServiceExtension中,需要和主程序进行一些数据交换,这里通过共享分组的方式实现,以此来了解主程序的状态

import UserNotifications
import AudioToolbox
import AVFAudio
import UIKit

class NotificationService: UNNotificationServiceExtension {

    var contentHandler: ((UNNotificationContent) -> Void)?
    var bestAttemptContent: UNMutableNotificationContent?
    
    private let keyStartCallTime = "keyStartCallTime"
    private let groupName = "group.xxp.ring.callNotification"
    private let callKeyEnterToForeground = "enterToForground"
    
    //最长显示通知时长设置为30秒
    let maxNotifyTime = 30

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        self.contentHandler = contentHandler
        bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
        
        if let bestAttemptContent = bestAttemptContent {
            // Modify the notification content here...
            bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
            if let contType = (request.content.userInfo["info"] as? Dictionary<String , Any>)?["type"] as? Int32{
                print("当前消息类型:\(contType)")
                if contType == 1 {
                    //音视频通话请求
                    if isAppInForground() || isCallNotifyActive() {
                        //APP在前台时或者通话已经被激活了内部处理自动处理
                        contentHandler(request.content)
                        return
                    }
                    //通话邀请
                    bestAttemptContent.title = "\(bestAttemptContent.title)"
                    bestAttemptContent.interruptionLevel = .timeSensitive
                    //创建声音
                    var soundID: SystemSoundID = SystemSoundID(clamping: 999)
                    // 播放声音
                    if let url = Bundle.main.url(forResource: "ring", withExtension: "mp3"){
                        AudioServicesCreateSystemSoundID(url as CFURL, &soundID)
                        AudioServicesAddSystemSoundCompletion(soundID, nil, nil, { sourceId, unsafeMutableRawPointer in
                        }, nil)
                        AudioServicesPlaySystemSound(soundID)
                        AudioServicesPlaySystemSoundWithCompletion(soundID) {
                            if self.isCallNotifyActive(){
                                print("没有结束,继续播放")
                                AudioServicesPlaySystemSound(soundID)
                            }
                        }
                    } else {
                        print("没有找到铃声")
                    }
                    
                    contentHandler(bestAttemptContent)
                    //记录开始时间
                    self.setCallStartTime()
 
                    //持续震动逻辑
                    while self.isCallNotifyActive(){
                        AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
                        sleep(1)
                    }
                    self.setCallStartTime(clean: true)
                    AudioServicesDisposeSystemSoundID(soundID)
                }
                else if contType == 2 {
                    //通话结束
                    print("通话结束重置任务")
                    setCallStartTime(clean: true)
                    contentHandler(request.content)
                } else {
                    contentHandler(request.content)
                }
            } else {
                contentHandler(request.content)
            }
        }
    }
    
    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)
        }
    }
    
    //通话通知是否仍处于激活状态
    func isCallNotifyActive() -> Bool{
        return Int64(Date.now.timeIntervalSince1970) - getStartCallTime() < maxNotifyTime && !self.isAppInForground()
    }
    
    func getStartCallTime() -> Int64 {
        UserDefaults(suiteName: self.groupName)?.object(forKey: keyStartCallTime) as? Int64 ?? 0
    }
    
    func setCallStartTime(clean : Bool = false) {
        if clean {
            UserDefaults(suiteName: self.groupName)?.removeObject(forKey: keyStartCallTime)
        } else {
            let startTime : Int64 = Int64(Date.now.timeIntervalSince1970)
            UserDefaults(suiteName: self.groupName)?.set(startTime, forKey: keyStartCallTime)
        }
    }
    
    //APP是否在前台
    func isAppInForground() -> Bool{
        UserDefaults(suiteName: self.groupName)?.bool(forKey: callKeyEnterToForeground) ?? false
    }

}

无法拦截通知处理问题排查

  • 检查拦截服务的Minimum Deployments版本,必须要小于你运行手机或者模拟器的版本,它默认指定的版本可能是最新版本,修改版本号即可在这里插入图片描述

  • 推送时,依赖于ios本身的APNS代理,需要在推送内容中设置mutable-content字段,1表示开启,0表示关闭,只有设置了1才会被UNNotificationServiceExtension所拦截,参考Apple官方文档说明

通知服务应用扩展标志。如果值为1,系统会在传递之前将通知传递给您的通知服务应用扩展。使用您的扩展程序修改通知的内容。

  • 检查是否创建了多个Notification Service Extension,测试发现只有第一个Notification Service Extension会生效,其余Notification Service Extension将不会被使用

其它问题

  • 测试下来UNNotificationServiceExtension只有第一个会生效,后续的UNNotificationServiceExtension都不会被调用

  • 铃声文件,放在同级目录下即可在这里插入图片描述

  • 对于多通知下发处理,测试发现当上一个通知没有被处理之前,下一个通知抵达后将会排队,不会直接回调didReceive方法,直到上一个contentHandler(UNNotificationContent)被调用。以至于在呼叫响铃以及挂断停止响铃上难以下手。但是在测试下发现以下方式是可用的

先执行contentHandler(UNNotificationContent)回调,让UNNotificationServiceExtension不至于无法处理下一个通知,然后在当前线程上进行阻塞,这可以有效阻止主程序再次被挂起,导致响铃或者震动被中断,两个通知下发后,他们使用的似乎不是同一个对象,原理暂时不清楚,有知道的可以告知

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值