At the end of the previous part you had a working application, but it did not do much. All user interaction consisted of a click on a single button. This part will add more functionality.
Note: I have a new post on this topic, updated for Swift 5.1 and SwiftUI.
Add a Text Field
Right now the argument you are passing into the say
command is fixed. It would be better to have a field where the user can enter arbitrary text that will be passed to the command.
In the file sidebar, select the ‘MainMenu.xib’ file and then move the ‘Talk’ button to the top right corner of the ‘SayThis’ window. As you move the button around the window blue lines will appear to guide you to the proper distances of the edges. Use these blue lines. Their alignments will help you later.
Next, use the object library in the lower right of the Xcode window to find a ‘Text Field’ object.
Drag the text field to your ‘SayThis’ window. Use the blue guide lines to align it in the top right corner and then extend it to the right until the blue lines show you it is right distance from the button.
You could now build the app and see the text field is there. You can even enter text. However, clicking the ‘Talk’ button will still only say “hello world” since you have not changed the code yet.
In your code you need to get the text from the text field. For that you need some reference to the text field object in the window. Create this ‘outlet’ the same way you hooked up the button action. First make sure you have a two part Xcode window. If you have closed the second pane, you can get it back by clicking the ‘Assistant Editor’ button in the top right window tool bar.
And then choosing the ‘AppController’ file for the second pane.
Then drag while holding the ctrl-key or with the secondary (right) mouse button from the text field to the code in the second pane. A blue line will appear to show the connection and a label ‘Insert Outlet or Action’ will show you what will happen and where. Let go of the drag below the line @IBOutlet weak var window: NSWindow!
.
Then a small panel will appear asking for a name for the outlet connection. Enter sayThisTextField
.
This will insert the following line of code:
@IBOutlet weak var sayThisTextField: NSTextField!
In code terms this is a property of the AppController
class. The @IBOutlet
label tells Xcode that this property can be connected to a UI element, same as the @IBAction
for the talk
method. But this does not only create the proper code in the class to declare the property. The xib file also stores the instructions that this property will be set with the proper reference when the application loads. Same as the window
property above which was part of the default template. Either way you look at it, you can now use the sayThisTextField
property to get data from the text field.
Read More: Connecting Objects to Code – Mac Developer Library
Read More: Properties – Swift Language Guide
Change the talk
method like this:
@IBAction func talk(sender: NSButton) {
let path = "/usr/bin/say"
let textToSay = sayThisTextField.stringValue
let arguments = [textToSay]
Leave the remainder of the method as it is.
Build and run the application, then enter some english text in the text field and click the ‘Talk’ button.
Make the Talk Button React to Return Key
If you are like me, you may have tried to hit the ‘return’ key to make the application say the text. It is a standard OS X behavior, that the return key activates the default button. But you need to configure our UI the right way for that to work. (Imagine a more complex application where there were more buttons. You need to tell the application which button is the default.)
To do this, select the ‘Talk’ button (1), then make sure the ‘Attributes Inspector’ tab is selected on the right side of the Xcode window (2). Among the button attributes is one called ‘Key Equivalent’. Select the field next to this label and hit the return key. It should show the return icon as the key equivalent and turn the ‘Talk’ button blue. Now you can just enter text and hit the return key to activate the ‘Talk’ button and thus our action.
Re-sizing the Window
When you build and run our application you can resize the window, but the text field and the button just stay where they are. This is not the right behavior. You can set re-sizing behavior in Xcode. Once again you need to ctrl or right drag from the object, but this time just drag towards the edge of the window which contains the object. The panel that pops up will then have options on how you want to ‘pin’ the object in the enclosing container.
Movie demonstrating how to setup Size Constraints
In the movie, normal clicks are shown as black circles and ctrl or right clicks are shown as blue circles.
You need to drag from the talk button to the right margin and upper margin. Then from the text field to the upper margin and left margin. Finally drag from the text field to the button and fix that distance as well.
When you then rebuild, then button and text field will react as you expect. You can inspect and change or delete the constraints for an object by clicking on the ruler icon in the Inspector pane on the right of the Xcode window. You can also click the constraint lines in the interface directly but they are small and hard to hit with the mouse.
Setting up the constraints manually is powerful, but tedious and error-prone. You can also let Xcode suggest constraints. To do this select an object and choose “Editor > Resolve Auto Layout Issues > Reset to Suggested Constraints” from the menu.
This will use the blue guide lines as the default constraints which is why it is important that you use them when placing objects. You can choose to reset the constraints of the selected object(s) or all objects (views) in a window. This will overwrite all constraints you may have set manually!
Read more: Auto Layout Guide – Mac Developer Library
Add a Progress Indicator
The de-activation of the talk button from in the previous part still works. But it would be nice to have some indication that the system is working on something, especially on longer texts.
Use the object library to add an “Indeterminate Circular Progress Indicatior” to your window. Place it above the right end of the text field and use the “Reset to Suggested Constraints” menu to set up the re-sizing behavior. Then disable the “Display when Stopped” behavior in the attributes inspektor.
To give the progress indicator the instructions to start and stop the animation you need to hook it up to our AppController
class. ctrl- or right-drag from the progress indicator to the AppController class to insert a new @IBOutlet
and name it sayProgress
.
Then change the talk
action method like this: (add the two lines starting with sayProgress
)
@IBAction func talk(sender: NSButton) {
let path = "/usr/bin/say"
let textToSay = sayThisTextField.stringValue
let arguments = [textToSay]
sender.enabled = false
sayProgress.startAnimation(self)
let task = NSTask.launchedTaskWithLaunchPath(path, arguments: arguments)
task.waitUntilExit()
sender.enabled = true
sayProgress.stopAnimation(self)
}
The progress indicator object has actions named startAnimation
and stopAnimation
. So you can tell them to take that action. Just like your talk
action method you have to pass one parameter indicating the sender. When using other objects’ actions you would usually give self
to pass a reference to the calling object.
Note that you use the sender
parameter to address the talk button. Since the talk button is the only UI element that can is hooked up to send the talk action this is safe to do. You can also hook up the talk button as an @IBOutlet
and address it directly that way.
Note: in the Xcode split view you can hover over the grey circles next to the @IB...
labels and Xcode will indicate the objects this action or outlet is hooked up to. If an outlet or action is not hooked up then the grey circle will be empty.
A Choice of Voices
The default OS X text to speech voice is called ‘Alex’, but there are many more. You can configure the avilable voices in the “Dictation & Speech” pane in System Preferences. When you click on the popup next to System Voice and then select “Customize” you can choose voices for different languages. Note that the download for the enhanced voices can be quite large.
You can also have the say
command list available voices:
say -v ?
You can tell the say
command to use a different voice with the -v
option
say -v Allison "hello world"
To give the user an option to choose from a list of voices, search for “popup” in the Object Library and drag the normal Pop Up Button to your SayThis Window. Also search for “label” and drag a label next to it. Change the Label to show “Voice:” and align both with other items and the blue guides. Set the suggest constraints.
Next create an @IBOutlet
for the popup button, call it voicePopup
.
Now, you can double click the popup button to edit its contents. There are three defaults and you can add more by dragging a “menu item” from the object library on to the popup button.
Then you need to add to the talk
method to get the information from the popup button and pass it into the task.
@IBAction func talk(sender: NSButton) {
let path = "/usr/bin/say"
let textToSay = sayThisTextField.stringValue
var arguments = [textToSay]
if let voice = voicePopup.titleOfSelectedItem {
arguments += ["-v", voice]
}
sender.enabled = false
sayProgress.startAnimation(self)
let task = NSTask.launchedTaskWithLaunchPath(path, arguments: arguments)
task.waitUntilExit()
sender.enabled = true
sayProgress.stopAnimation(self)
}
First you have to change the arguments
assignment to var
, since you might change it later. Then you get the titleOfSelectedItem
property from the voicePopup
However, this is an ‘optional’, which means the value may be nothing. Swift requires you to deal with optional values, so you use a ‘conditional assignment’: if let voice =...
This construct means “ if voicePopup.titleOfSelectedItem
has a value, assign it to voice
and do the following”.
Read More: Optionals – Swift Language Guide
If there is a value from the popup, then append it to the arguments
array. (Thankfully the say
command does not care about order of the options.) Otherwise it adds no arguments and uses the default voice, which you set in System Preferences.
Save the Voice choice
Every time to quit and restart the app the setting on the voice popup reverts to the first item. There may be use cases where this is appropriate. However, often you want to store settings between application launches. The class to use for that called NSUserDefaults
. However, this is such a common task, that you do not even need to write code. Select the popup button and then click on the square spiral icon to show the Bindings Inspector. Click the triangle next to ‘Selected Value’ to expose the settings for that. Check “Bind to” and make sure “Shared User Defaults Controller” is selected in this popup. Leave ‘values’ in the controller key field and enter ‘voice’ as the model key path.
With this configuration, the popup button to “binds” its selected value to the key ‘voice’ of the property ‘value’ of the ‘Shared User Defaults Controller’. This binding goes both ways, the popup button will read its initial state when starting the application from there and will write it back to there when the user changes it.
Re-build and launch the application, change the voice, quit and re-launch. The setting will persist.
Read more: User Defaults and Bindings – Mac Developer Library
Now open Terminal and enter:
defaults read com.scriptingosx.SayThis
But instead of com.scriptingosx.SayThis
use the full bundle identifier you entered when creating the project in the first part. If you do not remember what you chose, you can select the blue ‘SayThis’ project icon in the left side file selector to see all you project information.
The defaults
command will return the following:
$ defaults read com.scriptingosx.SayThis
{
voice = Allison;
}
Now quit the application and enter this in Terminal:
defaults write com.scriptingosx.SayThis voice "Karen"
Then re-launch the application and it will have preset the popup to “Karen”.
The actual setting is stored in a property list file in ~/Library/Preferences/com.scriptingosx.SayThis.plist
. However, it is not recommended to directly manipulate this file. The system caches preference files in memory and the file may be out of date or overwritten without your changes being read. If you access the data through the defaults
command you will get the current status. If you need to reset the settings of your app for testing, you can use
defaults delete com.scriptingosx.com
Note: preferences stored this way can be managed with custom configuration profiles.
Awesome article! Both Part 1 and 2.
I’m getting lost when you add the PopUp menu variable.
Will you the finished code?
I have pushed the sample project to github here: https://github.com/scriptingosx/SayThis You should be able to see the steps in the articles in the git history.
Great. Now how can you do this in an XCTest class on iOS so that I can automatically create heap and leaks reports before the app quits?