Defer AVSpeechSynthesisVoice init to first speak() call

AVSpeechSynthesisVoice(language:) triggers a malloc double-free on
iOS 26 simulators when deserializing voice metadata during app launch.
Move voice resolution from init() to first speak() so the framework
call happens after the app is fully initialized.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-11 20:52:56 -05:00
parent 2a062cf484
commit a51d2abd47

View File

@@ -4,14 +4,20 @@ import AVFoundation
@MainActor @MainActor
final class SpeechService { final class SpeechService {
private let synthesizer = AVSpeechSynthesizer() private let synthesizer = AVSpeechSynthesizer()
private let spanishVoice: AVSpeechSynthesisVoice? private var spanishVoice: AVSpeechSynthesisVoice?
private var voiceResolved = false
private var audioSessionConfigured = false private var audioSessionConfigured = false
init() { init() {
spanishVoice = AVSpeechSynthesisVoice(language: "es-ES") // AVSpeechSynthesisVoice can trigger a malloc double-free on
// iOS 26 simulators when deserializing voice metadata. Defer
// voice resolution to first use so the crash doesn't happen
// during app launch.
spanishVoice = nil
} }
func speak(_ text: String) { func speak(_ text: String) {
resolveVoiceIfNeeded()
configureAudioSession() configureAudioSession()
synthesizer.stopSpeaking(at: .immediate) synthesizer.stopSpeaking(at: .immediate)
let utterance = AVSpeechUtterance(string: text) let utterance = AVSpeechUtterance(string: text)
@@ -27,6 +33,12 @@ final class SpeechService {
synthesizer.stopSpeaking(at: .immediate) synthesizer.stopSpeaking(at: .immediate)
} }
private func resolveVoiceIfNeeded() {
guard !voiceResolved else { return }
voiceResolved = true
spanishVoice = AVSpeechSynthesisVoice(language: "es-ES")
}
private func configureAudioSession() { private func configureAudioSession() {
guard !audioSessionConfigured else { return } guard !audioSessionConfigured else { return }
do { do {