From a51d2abd47d983143bdd5797c41732cd9639ff21 Mon Sep 17 00:00:00 2001 From: Trey t Date: Sat, 11 Apr 2026 20:52:56 -0500 Subject: [PATCH] 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) --- Conjuga/Conjuga/Services/SpeechService.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Conjuga/Conjuga/Services/SpeechService.swift b/Conjuga/Conjuga/Services/SpeechService.swift index 5186b00..22a460b 100644 --- a/Conjuga/Conjuga/Services/SpeechService.swift +++ b/Conjuga/Conjuga/Services/SpeechService.swift @@ -4,14 +4,20 @@ import AVFoundation @MainActor final class SpeechService { private let synthesizer = AVSpeechSynthesizer() - private let spanishVoice: AVSpeechSynthesisVoice? + private var spanishVoice: AVSpeechSynthesisVoice? + private var voiceResolved = false private var audioSessionConfigured = false 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) { + resolveVoiceIfNeeded() configureAudioSession() synthesizer.stopSpeaking(at: .immediate) let utterance = AVSpeechUtterance(string: text) @@ -27,6 +33,12 @@ final class SpeechService { synthesizer.stopSpeaking(at: .immediate) } + private func resolveVoiceIfNeeded() { + guard !voiceResolved else { return } + voiceResolved = true + spanishVoice = AVSpeechSynthesisVoice(language: "es-ES") + } + private func configureAudioSession() { guard !audioSessionConfigured else { return } do {