Swift实现推送通知触发全场景文本转语音功能可行性咨询
问题背景
我正在开发iOS项目,需要实现这么个功能:APP收到推送通知时自动触发文本转语音(TTS),要求:
- 通知先本地存储,再用
UNMutableNotificationContent生成本地通知展示 - TTS必须走铃声音频通道,不能用媒体音频
- 和系统通知的
.sound属性一样,要支持前台、后台甚至APP被杀死的状态下正常运行
之前试了两种音频会话类别都不行:
- 用
playback类别:走媒体音频通道,不符合铃声音频的要求 - 用
ambient类别:后台或APP被杀后完全没法运行TTS
下面是我在AppDelegate的didReceiveRemoteNotification里写的代码:
if let tts = userInfo["text"] as? String { do { try audioSession.setActive(true) try audioSession.setCategory(AVAudioSession.Category.ambient, options: [.duckOthers]) } catch { print("Unable to set audio session category: %@", error) } let utterance = AVSpeechUtterance(string: userInfo["text"] as! String) utterance.voice = AVSpeechSynthesisVoice(language: "id-ID") synthesizer.speak(utterance) }
解决思路和代码
要满足需求,得解决两个核心问题:后台/被杀状态下触发TTS、用铃声音频通道,具体这么做:
1. 换用正确的音频会话类别
把ambient改成alert类别,这个类别专门给通知、提醒类音频用,走铃声音频通道,还能在后台激活:
do { try audioSession.setCategory(.alert, options: []) try audioSession.setActive(true) } catch { print("音频会话设置失败: \(error)") }
.alert属于系统铃声音频通道,不会占用媒体播放的通道,完全符合要求- 不用加
.duckOthers,通知类音频本来就是短暂提醒,不需要压低其他音频
2. 适配后台/被杀状态的触发逻辑
didReceiveRemoteNotification在APP被杀后根本不会触发,得靠UNNotificationServiceExtension来处理推送并调用TTS:
第一步:添加通知服务扩展
- Xcode里选
File > New > Target,找到Notification Service Extension - 随便起个名字(比如
TTSNotificationExtension),激活这个扩展
第二步:在扩展里写TTS逻辑
把NotificationService.swift的代码换成下面这样:
import UserNotifications import AVFoundation class NotificationService: UNNotificationServiceExtension { var contentHandler: ((UNNotificationContent) -> Void)? var bestAttemptContent: UNMutableNotificationContent? let synthesizer = AVSpeechSynthesizer() let audioSession = AVAudioSession.sharedInstance() override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) guard let content = bestAttemptContent, let ttsText = content.userInfo["text"] as? String else { contentHandler(request.content) return } // 配置音频会话 do { try audioSession.setCategory(.alert, options: []) try audioSession.setActive(true) } catch { print("音频会话设置失败: \(error)") contentHandler(content) return } // 生成语音并播放 let utterance = AVSpeechUtterance(string: ttsText) utterance.voice = AVSpeechSynthesisVoice(language: "id-ID") utterance.rate = 0.5 // 语速可以自己调整 // 监听语音播放完成,确保通知正常展示 synthesizer.delegate = self synthesizer.speak(utterance) // 超时兜底:5秒后不管语音有没有播完都展示通知 DispatchQueue.main.asyncAfter(deadline: .now() + 5) { contentHandler(self.bestAttemptContent ?? request.content) try? self.audioSession.setActive(false) } } override func serviceExtensionTimeWillExpire() { // 扩展快到期了,直接展示通知 try? audioSession.setActive(false) contentHandler?(bestAttemptContent ?? UNNotificationContent()) } } extension NotificationService: AVSpeechSynthesizerDelegate { func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { // 语音播完了,展示通知并关闭音频会话 contentHandler?(bestAttemptContent ?? UNNotificationContent()) try? audioSession.setActive(false) } }
第三步:配置推送Payload
推送的内容里必须加mutable-content: 1,不然系统不会调用你的扩展:
{ "aps": { "alert": "新通知提醒", "mutable-content": 1 }, "text": "这里是要转成语音的文本内容" }
3. 前台状态的适配
前台状态下didReceiveRemoteNotification能正常触发,直接复用上面的音频会话和TTS逻辑就行,记得在语音播完后关掉音频会话:
// 先确保你的类遵守AVSpeechSynthesizerDelegate class AppDelegate: UIResponder, UIApplicationDelegate, AVSpeechSynthesizerDelegate { let synthesizer = AVSpeechSynthesizer() let audioSession = AVAudioSession.sharedInstance() // ...其他代码 func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if let ttsText = userInfo["text"] as? String { do { try audioSession.setCategory(.alert, options: []) try audioSession.setActive(true) } catch { print("音频会话设置失败: \(error)") completionHandler(.failed) return } let utterance = AVSpeechUtterance(string: ttsText) utterance.voice = AVSpeechSynthesisVoice(language: "id-ID") synthesizer.delegate = self synthesizer.speak(utterance) } completionHandler(.newData) } // 语音播完后关闭音频会话 func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { try? audioSession.setActive(false) } }
重要提醒
.alert音频会话是核心,没有它就没法在后台/被杀状态下用铃声音频播TTS- 通知服务扩展是APP被杀后唯一能执行代码的入口,必须用它来触发TTS
- 推送Payload里的
mutable-content: 1不能少,否则扩展不会被调用
内容的提问来源于stack exchange,提问作者Nile
相关产品推荐
相关产品推荐

