Making A Mac App Scriptable Tutorial
Allow users to write scripts to control your OS X app – giving it unprecedented usability. Discover how in this “Making a Mac App Scriptable Tutorial”. By Sarah Reichelt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Making A Mac App Scriptable Tutorial
30 mins
Working With Nested Objects
In the sample app, the second column displays a list of tags assigned to each task. So far, you have no way of working with them via scripts – time to fix that!
Object specifiers can handle a hierarchy of objects. That’s what you have here, with the application owning the tasks and each task owning its tags.
As with the Task
class, you need to make the Tag
scriptable.
Open Tag.swift and make the following changes:
- Change the class definition line to
@objc(Tag) class Tag: NSObject {
- Add the
override
keyword toinit
. - Add the object specifier method:
override var objectSpecifier: NSScriptObjectSpecifier {
// 1
guard let task = task else { return NSScriptObjectSpecifier() }
// 2
guard let taskClassDescription = task.classDescription as? NSScriptClassDescription else {
return NSScriptObjectSpecifier()
}
// 3
let taskSpecifier = task.objectSpecifier
// 4
let specifier = NSUniqueIDSpecifier(containerClassDescription: taskClassDescription,
containerSpecifier: taskSpecifier, key: "tags", uniqueID: id)
return specifier
}
The above code is relatively straightforward:
- Check that the tag has an assigned task.
- Check that the task has a class description of the correct class.
- Get the object specifier for the parent task.
- Construct the object specifier for the tag contained inside the task and return it.
Add the following to the SDEF file at the Insert tag class here
comment:
<class name="tag" code="TaGg" description="A tag" inherits="item" plural="tags">
<cocoa class="Tag"/>
<property name="id" code="ID " type="text" access="r"
description="The unique identifier of the tag.">
<cocoa key="uniqueID"/>
</property>
<property name="name" code="pnam" type="text" access="rw"
description="The name of the tag.">
<cocoa key="name"/>
</property>
</class>
This is very similar to the data for the Task
class, but a tag only has two exposed properties: id
and name
.
Now the Task
section has to be edited to indicate that it contains tag elements.
Add the following code to the Task class XML, at the Insert element of tags here
comment:
<element type="tag" access="rw">
<cocoa key="tags"/>
</element>
Quit the app, then build and run the app again.
Go back to the Script Editor; if the Scriptable Tasks dictionary is open, close and re-open it. See if it contains information about tags.
If not, remove the Scriptable Tasks entry from the Library and add it again by dragging the app into the window:
Try one of the following scripts:
tell application "Scriptable Tasks"
get the name of every tag of task 1
end tell
or
app = Application("Scriptable Tasks");
app.tasks[0].tags.name();
The app now lets you retrieve tags – but what about adding new ones?
You may have noticed in Tag.swift that each Tag
object has a weak reference to its owning task. That helps create the links when getting the object specifier, so this task property must be set when assigning a new tag to a task.
Open Task.swift and add the following method to the Task
class:
override func newScriptingObject(of objectClass: AnyClass,
forValueForKey key: String,
withContentsValue contentsValue: Any?,
properties: [String: Any]) -> Any? {
let tag: Tag = super.newScriptingObject(of: objectClass, forValueForKey: key,
withContentsValue: contentsValue,
properties: properties) as! Tag
tag.task = self
return tag
}
This method is sent to the container of the new object, which why you put it into the Task
class and not the Tag
class. The call is passed to super
to get the new tag, and then the task property is assigned.
Quit and build and run your app. Now run the sample script 6. Tasks With Tags.scpt which lists tag names, lists the tasks with a specified tag, and deletes and create tags.
Adding Custom Commands
There is one more step you can take when making an app scriptable: adding custom commands. In earlier scripts, you toggled the completed
flag of a task directly. But wouldn’t it be better – and safer – if scripts didn’t change the property directly, but instead used a command to do this?
Consider the following script:
mark the first task as "done"
mark task "Feed the cat" as "not done"
I’m sure you’re already reaching for the SDEF file and you would be correct: the command has to be defined there first.
There are two steps that need to happen here:
- Tell the application that this command exists and what its parameters will be.
- Tell the Task class that it responds to the command and what method to call to implement it.
Inside the Scriptable Tasks suite, but outside any class, add the following at the Insert command here comment:
<command name="mark" code="TaSktext">
<direct-parameter description="One task" type="task"/>
<parameter name="as" code="DFLG" description="'done' or 'not done'" type="text">
<cocoa key="doneFlag"/>
</parameter>
</command>
“Wait a minute!” you say. “Earlier you said that codes had to be four characters, and now I have one with eight? What’s going on here?”
When defining a method, you provide a two part code. This one combines the codes or types of the parameters – in this case a Task
object with some text.
Inside the Task
class definition, at the Insert responds-to command here comment, add the following code:
<responds-to command="mark">
<cocoa method="markAsDone:"/>
</responds-to>
Now head back to Task.swift and add the following method:
func markAsDone(_ command: NSScriptCommand) {
if let task = command.evaluatedReceivers as? Task,
let doneFlag = command.evaluatedArguments?["doneFlag"] as? String {
if self == task {
if doneFlag == "done" {
completed = true
} else if doneFlag == "not done" {
completed = false
}
// if doneFlag doesn't match either string, leave un-changed
}
}
}
The parameter to markAsDone(_:)
is an NSScriptCommand
which has two properties of interest: evaluatedReceivers
and evaluatedArguments
. From them, you try to get the task and the string parameter and use them to adjust the task accordingly.
Quit and build and run your app again. Check the dictionary in the Script Editor, and delete and re-import it if the mark
command is not showing:
You should now be able to run the 7. Custom Command.scpt scripts and see your new command in operation.
mark
command does not work in JavaScript. I have added manual toggling of the completed
property to the JavaScript version of 7. Custom Command.scpt but left the original there too. Hopefully it will work after an update.