Skip to content

Commit 1f18118

Browse files
committed
fix(ios): nomatch event firing on iOS 18+
1 parent dfa40de commit 1f18118

3 files changed

+35
-7
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,9 @@ ExpoSpeechRecognitionModule.start({
237237
// The maximum number of alternative transcriptions to return.
238238
maxAlternatives: 1,
239239
// [Default: false] Continuous recognition.
240-
// If false on iOS, recognition will run until no speech is detected for 3 seconds.
240+
// If false:
241+
// - on iOS 17-, recognition will run until no speech is detected for 3 seconds.
242+
// - on iOS 18+ and Android, recognition will run until a final result is received.
241243
// Not supported on Android 12 and below.
242244
continuous: true,
243245
// [Default: false] Prevent device from sending audio over the network. Only enabled if the device supports it.

ios/ExpoSpeechRecognitionModule.swift

+28-5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ public class ExpoSpeechRecognitionModule: Module {
4141
// This is a temporary workaround until the issue is fixed in a future iOS release
4242
var hasSeenFinalResult: Bool = false
4343

44+
// Hack for iOS 18 to avoid sending a "nomatch" event after the final-final result
45+
// Example event order emitted in iOS 18:
46+
// [
47+
// { isFinal: false, transcripts: ["actually", "final", "results"], metadata: { duration: 1500 } },
48+
// { isFinal: true, transcripts: [] }
49+
// ]
50+
var previousResult: SFSpeechRecognitionResult?
51+
4452
public func definition() -> ModuleDefinition {
4553
// Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument.
4654
// Can be inferred from module's class name, but it's recommended to set it explicitly for clarity.
@@ -130,6 +138,9 @@ public class ExpoSpeechRecognitionModule: Module {
130138
do {
131139
let currentLocale = await speechRecognizer?.getLocale()
132140

141+
// Reset the previous result
142+
self?.previousResult = nil
143+
133144
// Re-create the speech recognizer when locales change
134145
if self.speechRecognizer == nil || currentLocale != options.lang {
135146
guard let locale = resolveLocale(localeIdentifier: options.lang) else {
@@ -358,12 +369,14 @@ public class ExpoSpeechRecognitionModule: Module {
358369

359370
func sendErrorAndStop(error: String, message: String) {
360371
hasSeenFinalResult = false
372+
previousResult = nil
361373
sendEvent("error", ["error": error, "message": message])
362374
sendEvent("end")
363375
}
364376

365377
func handleEnd() {
366378
hasSeenFinalResult = false
379+
previousResult = nil
367380
sendEvent("end")
368381
}
369382

@@ -422,11 +435,19 @@ public class ExpoSpeechRecognitionModule: Module {
422435
}
423436

424437
if isFinal && results.isEmpty {
425-
// https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/nomatch_event
426-
// The nomatch event of the Web Speech API is fired
427-
// when the speech recognition service returns a final result with no significant recognition.
428-
sendEvent("nomatch")
429-
return
438+
// Hack for iOS 18 to avoid sending a "nomatch" event after the final-final result
439+
var previousResultWasFinal = false
440+
if #available(iOS 18.0, *), let previousResult = previousResult {
441+
previousResultWasFinal = previousResult.speechRecognitionMetadata?.speechDuration ?? 0 > 0
442+
}
443+
444+
if !previousResultWasFinal || previousResult?.transcriptions.isEmpty {
445+
// https://developer.mozilla.org/en-US/docs/Web/API/SpeechRecognition/nomatch_event
446+
// The nomatch event of the Web Speech API is fired
447+
// when the speech recognition service returns a final result with no significant recognition.
448+
sendEvent("nomatch")
449+
return
450+
}
430451
}
431452

432453
sendEvent(
@@ -436,6 +457,8 @@ public class ExpoSpeechRecognitionModule: Module {
436457
"results": results.map { $0.toDictionary() },
437458
]
438459
)
460+
461+
previousResult = result
439462
}
440463

441464
func handleRecognitionError(_ error: Error) {

src/ExpoSpeechRecognitionModule.types.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,10 @@ export type ExpoSpeechRecognitionOptions = {
149149
*
150150
* Not supported on Android 12 and below.
151151
*
152-
* If false on iOS, recognition will run until no speech is detected for 3 seconds.
152+
* If false, the behaviors are the following:
153+
*
154+
* - on iOS 17-, recognition will run until no speech is detected for 3 seconds.
155+
* - on iOS 18+ and Android, recognition will run until a result with `isFinal: true` is received.
153156
*/
154157
continuous?: boolean;
155158
/** [Default: false] Prevent device from sending audio over the network. Only enabled if the device supports it.

0 commit comments

Comments
 (0)