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.

5 (1) · 1 Review

Save for later
Share
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Making Your App Scriptable

The scripting definition file of your app defines what the app can do; it’s a little like an API. This file lives in your app project and specifies several things:

  • Standard scripting objects and commands, such as window, make, delete, count, open and quit.
  • Your own scriptable objects, properties and custom commands.

In order to make classes in your app scriptable, there are a few changes you’ll need to make to the app.

First, the scripting interface uses Key-Value-Coding to get and set the properties of objects. In Objective-C, all objects conformed to the KVC protocol automatically, but Swift objects don’t do so unless you make them subclasses of NSObject.

Next, scriptable classes need an Objective-C name that the scripting interface can recognize. To avoid namespace conflicts, Swift object names are mangled to give a unique representation. By prefixing the class definitions with @objc(YourClassName), you give them a name that can be used by the scripting engine.

Scriptable classes need object specifiers to help locate a particular object within the application or parent object, and finally, the app delegate must have access to the data store so it can return the application’s data to the scripts.

You don’t necessarily have to start your own scripting definition file from scratch, as Apple provides a standard SDEF file that you can use. Look in the /System/Library/ScriptingDefinitions/ directory for CocoaStandard.sdef. Open this file in Xcode and have a look; it’s XML with specific headers, a dictionary and inside that, the Standard Suite.

This is a useful starting point, and you could copy and paste this XML into your own SDEF file. However, in the interest of clean code, it’s not a good idea to leave your SDEF file full of commands and objects that your app does not support. To this end, the sample project contains a starter SDEF file with all unnecessary entries removed.

Close CocoaStandard.sdef and open ScriptableTasks.sdef. Add the following code near the end at the Insert Scriptable Tasks suite here comment:

<!-- 1 -->
<suite name="Scriptable Tasks Suite" code="ScTa" description="Scriptable Tasks suite.">
  <!-- 2 -->
  <class name="application" code="capp" description="An application's top level scripting object.">
    <cocoa class="NSApplication"/>

    <!-- 3 -->
    <element type="task" access="r">
      <cocoa key="tasks"/>
    </element>
  </class>

  <!-- Insert command here -->

  <!-- 4 -->
  <class name="task" code="TaSk" description="A task item" inherits="item" plural="tasks">
      <cocoa class="Task"/>

      <!-- 5 -->
      <property name="id" code="ID  " type="text" access="r"
          description="The unique identifier of the task.">
          <cocoa key="id"/>
      </property>

      <property name="name" code="pnam" type="text" access="rw"
          description="The title of the task.">
          <cocoa key="title"/>
      </property>

      <!-- 6 -->
      <property name="daysUntilDue" code="CrDa" type="number" access="rw"
      description="The number of days before this task is due."/>
      <property name="completed" code="TrFa" type="boolean" access="rw"
      description="Has the task been completed?"/>
      
      <!-- 7 -->
      <!-- Insert element of tags here -->
      
      <!-- Insert responds-to command here -->
      
  </class>
  
  <!-- Insert tag class here -->
  
</suite>

This chunk of XML does a lot of work. Taking it bit by bit:

"ID " is read-only, as scripts should not change a unique identifier, but "pnam" is read-write. Both of these are text properties. The "pnam" property maps to the title property of the Task object.

  1. The outermost element is a suite, so your SDEF file now has two suites: Standard Suite and Scriptable Tasks Suite. Everything in the SDEF file needs a four-character code. Apple codes are nearly always in lower-case and you will use a few of them for specific purposes. For your own suites, classes and properties, it’s best to use a random mix of upper-case, lower-case and symbols to avoid conflicts.
  2. The next section defines the application and must use the code "capp". You must specify the class of the application; if you had subclassed NSApplication, you would use your subclass name here.
  3. The application contains elements. In this app, the elements are stored in an array called tasks in the app delegate. In scripting terms, elements are the objects that the app or other objects can contain.
  4. The last chunk defines the Task class that the application contains. The plural name for accessing multiples is tasks. The class in the app that backs this object type is Task.
  5. The first two properties are special. Look at their codes: "ID " and "pnam". "ID " (note the two spaces after the letters) identifies the unique identifier of the object. "pnam" specifies the name property of the object. You can access objects directly using either of these properties.

    "ID " is read-only, as scripts should not change a unique identifier, but "pnam" is read-write. Both of these are text properties. The "pnam" property maps to the title property of the Task object.

  6. The remaining two properties are a number property for daysUntilDue and a Boolean for completed. They use the same name in the object and the script, so you don’t need to specify the cocoa key.
  7. The “Insert…” comments are placeholders for when you need to add more to this file.

Open Info.plist, right-click in the blank space below the entries and select Add Row. Type an upper-case S and the list of suggestions will scroll to Scriptable. Select it and change the setting to YES.

Repeat this process to select the next item down: Scripting definition file name. Set this to the name of your SDEF file: ScriptableTasks.sdef

If you prefer to edit the Info.plist as source code, you can alternatively add the following entries inside the main dict:

<key>NSAppleScriptEnabled</key>
<true/>
<key>OSAScriptingDefinition</key>
<string>ScriptableTasks.sdef</string>

Now you have to modify the app delegate to handle requests that come via script.

Open AppDelegate.swift file and add the following to the end of the file:

extension AppDelegate {
  // 1
  override func application(_ sender: NSApplication, delegateHandlesKey key: String) -> Bool {
    return key == "tasks"
  }

  // 2
  func insertObject(_ object: Task, inTasksAtIndex index: Int) {
    tasks = dataProvider.insertNew(task: object, at: index)
  }

  func removeObjectFromTasksAtIndex(_ index: Int) {
    tasks = dataProvider.deleteTask(at: index)
  }
}

Here’s what’s going on in the code above:

  1. When a script asks for tasks data, this method will confirm that the app delegate can handle it.
  2. If a script tries to insert, edit or delete data, these methods will pass those requests along to dataProvider.

To make the Task model class available to the scripts, you have to do a bit more coding.

Open Task.swift and change the class definition line to the following:

@objc(Task) class Task: NSObject {

Xcode will immediately complain that init requires the override keyword, so let Fix-It do that. This is required as this class now has a superclass:

override init() {

Task.swift needs one more change: an object specifier. Insert the following method into the Task class:

override var objectSpecifier: NSScriptObjectSpecifier {
  // 1
  let appDescription = NSApplication.shared().classDescription as! NSScriptClassDescription

  // 2
  let specifier = NSUniqueIDSpecifier(containerClassDescription: appDescription,
                                      containerSpecifier: nil, key: "tasks", uniqueID: id)
  return specifier
}

Taking each numbered comment in turn:

  1. Get a description of the app’s class since the app is the container for tasks.
  2. Get a description of the task by id within the app. This is why the Task class has an id property – so that each task can be correctly specified.

You’re finally ready to start scripting your app!