From 177bb079a796cd766a31548927b71b3c7daf9575 Mon Sep 17 00:00:00 2001 From: Ismail Ashour Date: Sat, 28 Feb 2026 16:34:45 +0200 Subject: [PATCH] fix(ios): prevent crash when FlutterEngine is destroyed during background Add lifecycle guard to prevent NSInternalInconsistencyException "Sending a message before the FlutterEngine has been run" crash. The crash occurs when iOS tears down TTS resources (AVSpeechSynthesizer delegate callbacks) after the app enters background and the FlutterEngine is no longer running. The delegate methods unconditionally call channel.invokeMethod() which throws a fatal exception on a dead engine. Changes: - Add `isEngineAttached` flag to track engine lifecycle state - Add `safeInvokeMethod` helper that guards channel calls - Add `deinit` to stop synthesizer, nil delegate, and clear result callbacks before deallocation - Guard `didFinish` delegate with early return when engine is detached - Replace all 6 unguarded `channel.invokeMethod` calls with `safeInvokeMethod` Fixes #558 Fixes #595 Related: #318 Co-Authored-By: Claude Opus 4.6 --- ios/Classes/SwiftFlutterTtsPlugin.swift | 28 +++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/ios/Classes/SwiftFlutterTtsPlugin.swift b/ios/Classes/SwiftFlutterTtsPlugin.swift index 7b18a353..9d76ce31 100644 --- a/ios/Classes/SwiftFlutterTtsPlugin.swift +++ b/ios/Classes/SwiftFlutterTtsPlugin.swift @@ -28,6 +28,21 @@ public class SwiftFlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizer var channel = FlutterMethodChannel() + private var isEngineAttached = true + + private func safeInvokeMethod(_ method: String, arguments: Any?) { + guard isEngineAttached else { return } + self.channel.invokeMethod(method, arguments: arguments) + } + + deinit { + synthesizer.stopSpeaking(at: .immediate) + synthesizer.delegate = nil + speakResult = nil + synthResult = nil + isEngineAttached = false + } + init(channel: FlutterMethodChannel) { super.init() self.channel = channel @@ -431,6 +446,7 @@ public class SwiftFlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizer } public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { + guard isEngineAttached else { return } if shouldDeactivateAndNotifyOthers(audioSession) && self.autoStopSharedSession { do { try audioSession.setActive(false, options: .notifyOthersOnDeactivation) @@ -446,23 +462,23 @@ public class SwiftFlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizer self.synthResult!(1) self.synthResult = nil } - self.channel.invokeMethod("speak.onComplete", arguments: nil) + self.safeInvokeMethod("speak.onComplete", arguments: nil) } public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didStart utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onStart", arguments: nil) + self.safeInvokeMethod("speak.onStart", arguments: nil) } public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didPause utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onPause", arguments: nil) + self.safeInvokeMethod("speak.onPause", arguments: nil) } public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didContinue utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onContinue", arguments: nil) + self.safeInvokeMethod("speak.onContinue", arguments: nil) } public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didCancel utterance: AVSpeechUtterance) { - self.channel.invokeMethod("speak.onCancel", arguments: nil) + self.safeInvokeMethod("speak.onCancel", arguments: nil) } public func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOfSpeechString characterRange: NSRange, utterance: AVSpeechUtterance) { @@ -473,7 +489,7 @@ public class SwiftFlutterTtsPlugin: NSObject, FlutterPlugin, AVSpeechSynthesizer "end": String(characterRange.location + characterRange.length), "word": nsWord.substring(with: characterRange) ] - self.channel.invokeMethod("speak.onProgress", arguments: data) + self.safeInvokeMethod("speak.onProgress", arguments: data) } }