AVSpeechSynthesizer detect when the speech is finished

Daniel picture Daniel · May 31, 2016 · Viewed 7.1k times · Source

I just don't know how to do it...

I search here and at google and people talked about the AVSpeechSynthesizerDelegate but I wasn't able to use it.

I want to run a function exactly when the speech is over.

How can I achieve this? If I must use the delegate, how should I do it?

I tried that way:

func speechSynthesizer(synthesizer: AVSpeechSynthesizer, didFinishSpeechUtterance utterance: AVSpeechUtterance) {
    falando = false
    print("FINISHED")
}

This was one of the functions I found on the developer's doc, although the speech was told and nothing was printed.

I tried to put Class A : AVSpeechSynthesizerDelegate so then I would do Speech.delegate = self (Speech is an attribute of A, of type AVSpeechSynthesizer) but it said A does not conform to protocol NSObjectProtocol.

How can I run some function (even a print) as soon as the speech is over?

Thank you!

Answer

pbodsk picture pbodsk · May 31, 2016

A does not conform to protocol NSObjectProtocol means that your class must inherit from NSObject, you can read more about it here.

Now I don't know how you've structured your code, but this little example seems to work for me. First a dead simple class that holds the AVSpeechSynthesizer:

class Speaker: NSObject {
    let synth = AVSpeechSynthesizer()

    override init() {
        super.init()
        synth.delegate = self
    }

    func speak(_ string: String) {
        let utterance = AVSpeechUtterance(string: string)
        synth.speakUtterance(utterance)
    }
}

Notice that I set the delegate here (in the init method) and notice that it must inherit from NSObject to keep the compiler happy (very important!)

And then the actual delegate method:

extension Speaker: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        print("all done")
    }
}

And finally, I can use that class here, like so:

class ViewController: UIViewController {
    let speaker = Speaker()

    @IBAction func buttonTapped(sender: UIButton) {
        speaker.speak("Hello world")
    }
}

Which rewards me with

all done

in my console when the AVSpeechSynthesizer has stopped speaking.

Hope that helps you.

Update

So, time passes and in the comments below @case-silva asked if there was a practical example and @dima-gershman suggested to just use the AVSpeectSynthesizer directly in the ViewController.

To accommodate both, I've made a simple ViewController example here with a UITextField and a UIButton.

The flow is:

  1. You enter some text in the textfield (if not, a default value will be set)
  2. You press the button
  3. The button is disabled and the background color is changed (sorry, it was the best I could come up with :))
  4. Once speech is done, the button is enabled, the textfield is cleared and the background color is changed again.

Here's how it looks

A Simple UIViewController Example

import UIKit
import AVFoundation

class ViewController: UIViewController {

    //MARK: Outlets
    @IBOutlet weak var textField: UITextField!
    @IBOutlet weak var speakButton: UIButton!

    let synth = AVSpeechSynthesizer()

    override func viewDidLoad() {
        super.viewDidLoad()
        synth.delegate = self
    }

    @IBAction func speakButtonTapped(_ sender: UIButton) {
        //We're ready to start speaking, disable UI while we're speaking
        view.backgroundColor = .darkGray
        speakButton.isEnabled = false
        let inputText = textField.text ?? ""
        let textToSpeak = inputText.isEmpty ? "Please enter some text" : inputText

        let speakUtterance = AVSpeechUtterance(string: textToSpeak)
        synth.speak(speakUtterance)
    }
}

extension ViewController: AVSpeechSynthesizerDelegate {
    func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
        //Speaking is done, enable speech UI for next round
        speakButton.isEnabled = true
        view.backgroundColor = .lightGray
        textField.text = ""
    }
}

Hope that gives you a clue Case.