Commands

Syntax

All application commands have the same basic structure: a single, optional direct parameter, followed by zero or more named parameters specific to that command, followed by zero or more event attributes that determine how the Apple event is processed:

func commandName<T>(_ directParameter: Any = NoParameter,
                      namedParameter1: Any = NoParameter,
                      namedParameter2: Any = NoParameter,
                                   ...
                        requestedType: Symbol? = nil,
                            waitReply: Bool = true,
                          sendOptions: SendOptions? = nil,
                          withTimeout: NSTimeInterval? = nil,
                          considering: ConsideringOptions? = nil) throws -> T

For convenience, SwiftAutomation makes application commands available as methods on every object specifier. Due to the technical limitations of application dictionaries, the user must determine for themselves which commands can operate on a particular reference. Some applications document this information separately.

Examples

// tell app "TextEdit" to activate
TextEdit().activate()

// tell app "Finder" to get version
Finder().version.get()

// tell app "TextEdit" to open (POSIX file "/Users/jsmith/ReadMe.txt")
try TextEdit().open(URL(fileURLWithPath: "/Users/jsmith/ReadMe.txt"))

// tell app "Finder" to set name of file "foo.txt" of home to "bar.txt"
Finder().home.files["foo.txt"].name.set(to: "bar.txt")

// tell app "TextEdit" to count text of first document each paragraph
TextEdit().documents.first.text.count(each: TED.paragraph)

// tell app "Finder" to move every item of desktop to folder "Documents" of home
Finder().desktop.items.move(to: FINApp.home.folders["Documents"])

// tell app "TextEdit" to make new document at end of documents
TextEdit().documents.end.make(new: TED.document)

// tell app "Finder" to get items of home as alias list
Finder().home.items.get(returnType: FIN.alias)

Declaring a command's exact return type

Glue files define two methods for each application command: one that returns Any, which is used when the calling code does not declare a specific return type, and one that returns a generic type (T) that is used when the exact return type can be inferred.

Determining the actual type(s) of values returned by any given command is an exercise left to the user: the application's dictionary may be of some help, though any type information it contains is often incomplete or inaccurate; in addition, a command may return a value or list of values (or even nested lists) depending on whether it applies to a single application object or to multiple objects at once. The command(...)->Any form is particularly useful when testing and exploring an application's scripting interface, allowing you to see exactly what type(s) of value the application returns for that particular command:

TextEdit().documents.name.get()
// ["Untitled", "ReadMe.txt"]

TextEdit().documents.path.get()
// [MissingValue, "/Users/jsmith/ReadMe.txt"]

Once you know exactly what type of value to expect, adding an explicit cast to that type ensures that the command will always return a value of that type, or else throw a CommandError if the result descriptor returned by the application could not be coerced to that type:

TextEdit().documents.name.get() as [String]
// ["Untitled", "ReadMe.txt"]

TextEdit().documents.path.get() as [String]
// CommandError -1700: Can't make some data into the expected type.
//  
//   TextEdit().documents.path.get()
//
// Can't unpack value as Array<String>:
//
//   [MissingValue, "/Users/jsmith/ReadMe.txt"]
//
// Can't unpack item 1 as String.

In the second example above, casting the list of document paths to Array<String> fails because the unsaved document's path property contains 'missing value', not a string. Changing the return type to Array<Optional<String>> tells SwiftAutomation to accept either, converting 'missing value' to nil automatically:

TextEdit().documents.path.get() as [String?]
// [nil, Optional("/Users/jsmith/ReadMe.txt")]

The explicit cast only tells SwiftAutomation how to coerce and unpack the returned descriptor; it does not tell the application what type of value to return. Some application commands (e.g. get) may accept an optional as (keyAERequestedType) parameter indicating the type of value you want it to return, though this parameter is often not shown in the application's dictionary as AppleScript adds it automatically whenever its as operator is applied to a command. For example, Finder's get command normally returns an object specifier when getting a file or folder element:

tell app "Finder" to get home
-- folder "jsmith" of folder "Users" of startup disk of app "Finder"

finder.home.get() as FINItem
// Finder().startupDisk.folders["Users"].folders["jsmith"]

Adding an as operator tells Finder to return the command's result as a different type; for example, as an alias value:

tell app "Finder" to get home as alias
-- alias "Macintosh HD:Users:jsmith:"

To perform the same command in SwiftAutomation, pass the equivalent Symbol value as the command's requestedType: argument:

finder.home.get(requestedType: FIN.alias)
// URL(string:"file:///Users/jsmith")

Don't forget that the command's static result type will be Any unless an explicit cast is applied too:

finder.home.get(requestedType: FIN.alias) as URL
// URL(string:"file:///Users/jsmith")

Unlike AppleScript's as operator, which both adds an as parameter to the command and coerces its result, SwiftAutomation keeps the two separate for more precise control.

Also be aware that some application commands, e.g. CocoaScripting's save, may define an as: parameter that does not take a symbol name, in which case you should use that parameter as documented, not requestedType:.

[TO DO: the above is a bit scrappy and potentially confusing; unfortunately, that's due to inadequate AEOM specs and ill-considered implementation. SA only makes it possible to work with such a mess; it cannot fix it.]

Special cases

The following special-case behaviours are implemented for convenience:

The two forms are equivalent (SwiftAutomation converts the first form to the second behind the scenes) although the first form is preferred for conciseness. [TO DO: note that the first form only works when specifier has a targeted application object as its root; if the specifier is constructed from an untargeted App root, the second form must be used]

the specifier upon which it is called will be packed as the Apple event's "subject" attribute (keySubjectAttr).

Command errors

If a command fails due to an error raised by the target application or Apple Event Manager, or if a given parameter or attribute was not of a supported type, a CommandError containing the following properties is thrown:

Note to AppleScript users

Unlike AppleScript, which implicitly sends a get command to any unresolved application object references at the end of evaluating an expression, SwiftAutomation only resolves a reference when it receives an appropriate command. For example:

let o = TextEdit().documents

is not the same as:

set o to documents of application "TextEdit"

even though the two statements may look equivalent. In the Swift example, the value assigned to o is an instance of TEDSpecifier, TextEdit(name:"/Applications/TextEdit.app").documents, i.e. an object specifier. Whereas, in the AppleScript example, the evaluating the documents of application "TextEdit" expression not only constructs the same specifier, it also automatically sends a get event to the target application in order to retrieve the specified data, then assigns the result of that request to o:

set o to documents of application "TextEdit"
-- {document "Untitled" of application "TextEdit", document "Untitled 2" of application "TextEdit"}

This "implicit get" behavior is built directly into the AppleScript interpreter itself, and automatically applied to any specifier literal that does not already appear as a parameter to an application command, as a tell block target, or as the sole operand to AppleScript's' a reference to operator:

set o to a reference to documents of application "TextEdit"
-- every document of application "TextEdit"

In contrast, SwiftAutomation has no invisible "magic" behaviors attempting to infer your actual intent: it only ever sends an Apple event when you explicitly instruct it to do so:

let o = TextEdit().documents.get()
print(o)
// [TextEdit().documents["Untitled"], TextEdit().documents["Untitled 2"]]

New users coming from AppleScript or OO language backgrounds may find this unintuitive at first, but SwiftAutomation's clean separation between query construction and event dispatch ensures SwiftAutomation's behavior is completely straightforward and predictable, and avoids the hidden gotchas that can bite AppleScript users in various unexpected and confusing ways.