Another Way.
I wanted to post my solution to the silver and gold challenges because I went about them a slightly different way and thought it might help others who are brand new (like me) to macOS, Swift and xCode. There is a solution posted by @macintacos that works, but it addresses view components from the Document class and that seems to be a little bit in violation of the Model-View-Controller paradigm. So I offer this solution for comment/critique by the community.
First, I didn’t know how to refer to the buttons in code. So like others I figured it out by searching the web and realized that you need to add Interface Builder Outlets to the View Controller and connect them like the text area was connected in the chapter:
@IBOutlet weak var stopButton: NSButton!
@IBOutlet weak var speakButton: NSButton!
@IBOutlet weak var progressBar: NSProgressIndicator!
Then once the buttons and the progress bar were addressable in code I added these functions to the View Controller.
func configureButtonsWhileSpeaking() {
speakButton.isEnabled = false
stopButton.isEnabled = true
progressBar.isHidden = false
}
func configureButtonsWhileStopped() {
speakButton.isEnabled = true
stopButton.isEnabled = false
progressBar.isHidden = true
}
Just like @macintacos original post I configured the buttons using the viewDidLoad()
method in the View Controller. This method gets called right after the view is loaded so this sets the buttons to the right visibility before the user clicks anything. I also use this method to configure the delegate for the NSSpeechSynthesizer so that the View Controller is ‘listening’ when the delegate sends back signals.
override func viewDidLoad() {
super.viewDidLoad()
configureButtonsWhileStopped()
speechSynthesizer.delegate = self
}
A significant difference between my approach and others on this blog is how I control the progress bar.
func configureProgressBar(_ input: String) {
let contentsArray = input.components(separatedBy: " ")
let wordCount = contentsArray.count
progressBar.maxValue = Double(wordCount)
}
My approach from the code above is to count the words in the string that is about to be spoken and set the progress bar to that value. The NSSpeechSynthesizer provides a notification before speaking each word so word count makes an easy way to initialize the progress bar and then to fill it.
Next I added the protocol defined functions for the NSSpeechSynthesizerDelegate
func speechSynthesizer(_ sender: NSSpeechSynthesizer, willSpeakWord characterRange: NSRange, of string: String) {
configureButtonsWhileSpeaking()
progressBar.increment(by: 1.0)
}
func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
if finishedSpeaking {
configureButtonsWhileStopped()
progressBar.doubleValue = 0.0
}
}
When the willSpeakWord
signal arrives the buttons are configured and the progress bar is incremented by one word. Similarly, when the didFinishSpeaking
signal arrives the buttons are returned to normal and the progress bar is reset to 0.0
.
Now when the speakButton
is clicked the following code executes:
@IBAction func speakButtonClicked(_ sender: NSButton) {
if let contents = textView.string, !contents.isEmpty {
configureProgressBar(contents)
speechSynthesizer.startSpeaking(contents)
} else {
let defaultString = "The document is empty."
configureProgressBar(defaultString)
speechSynthesizer.startSpeaking(defaultString)
}
}
Here we set the progress bar correctly for the default text or the contents of the textView
object. I did not have to do anything with the buttons here because the delegate is keeping track of all that.
Interestingly, when the stopButton
is clicked it interrupts the NSSpeechSynthesizer and it does not actually finish. So it never sends the didFinishSpeakingSignal
. Thus, I call that signal manually.
@IBAction func stopButtonClicked(_ sender: NSButton) {
speechSynthesizer.stopSpeaking()
self.speechSynthesizer(speechSynthesizer, didFinishSpeaking: true)
}
I’ve worked through every challenge in the book up to this point and appreciate the way Big Nerd Ranch has taught Swift. Writing this helped me solidify some of the concepts around delegates in my mind…hope it helped you too.
The full code listing for my View Controller class is:
class ViewController: NSViewController, NSSpeechSynthesizerDelegate {
let speechSynthesizer = NSSpeechSynthesizer()
@IBOutlet var textView: NSTextView!
@IBOutlet weak var stopButton: NSButton!
@IBOutlet weak var speakButton: NSButton!
@IBOutlet weak var progressBar: NSProgressIndicator!
func configureButtonsWhileSpeaking() {
speakButton.isEnabled = false
stopButton.isEnabled = true
progressBar.isHidden = false
}
func configureButtonsWhileStopped() {
speakButton.isEnabled = true
stopButton.isEnabled = false
progressBar.isHidden = true
}
var contents: String? {
get {
return textView.string
}
set {
textView.string = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
configureButtonsWhileStopped()
speechSynthesizer.delegate = self
}
func configureProgressBar(_ input: String) {
let contentsArray = input.components(separatedBy: " ")
let wordCount = contentsArray.count
progressBar.maxValue = Double(wordCount)
}
@IBAction func speakButtonClicked(_ sender: NSButton) {
if let contents = textView.string, !contents.isEmpty {
configureProgressBar(contents)
speechSynthesizer.startSpeaking(contents)
} else {
let defaultString = "The document is empty."
configureProgressBar(defaultString)
speechSynthesizer.startSpeaking(defaultString)
}
}
@IBAction func stopButtonClicked(_ sender: NSButton) {
speechSynthesizer.stopSpeaking()
self.speechSynthesizer(speechSynthesizer, didFinishSpeaking: true)
}
func speechSynthesizer(_ sender: NSSpeechSynthesizer, willSpeakWord characterRange: NSRange, of string: String) {
configureButtonsWhileSpeaking()
progressBar.increment(by: 1.0)
}
func speechSynthesizer(_ sender: NSSpeechSynthesizer, didFinishSpeaking finishedSpeaking: Bool) {
if finishedSpeaking {
configureButtonsWhileStopped()
progressBar.doubleValue = 0.0
}
}
}