You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 08:08:15