Making Cocoa Application Scriptable Swift

Making Cocoa Application Scriptable Swift

Edit:

The main issue is that you don't actually use the with dname parameter in your script. It should be:

set returnValue to savedoc with dname testString

That said, the info below is still valid for creating a proper sdef and the other suggestions/examples may be helpful.


This is a basic example of passing a string in the evaluatedArguments of the NSScriptCommand and then returning that string as the result of the script command in Swift (you could return a boolean on success/failure of the command or any other type of result; and, actually, in your sdef you say you're going to return a boolean but your command is returning a string (text in sdef definitions)). Creating your sdef can be tricky. Your command's code should start with the suite's code and you can remove the id and optional parameter (if you omit the optional parameter, the default is that the parameter is required). If you do just need a single parameter you could also just use the direct-parameter instead.

You can download a demo project:

ScriptableSwift.zip

Here are the relevant bits (aside from the plist entries that you have correct in your tests).

ScriptableSwift.sdef

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary title="ScriptableSwift Terminology">
<suite name="ScriptableSwift Scripting Suite" code="SSss" description="Standard suite for application communication.">
<command name="save" code="SSssSave" description="Save something.">
<cocoa class="ScriptableSwift.SaveScriptCommand"/>
<parameter name="in" code="Fpat" type="text" description="The file path in which to save the document.">
<cocoa key="FilePath"/>
</parameter>
<result type="text" description="Echoes back the filepath supplied."/>
</command>
</suite>
</dictionary>

SaveScriptCommand.swift

import Foundation
import Cocoa

class SaveScriptCommand: NSScriptCommand {
override func performDefaultImplementation() -> AnyObject? {
let filePath = self.evaluatedArguments!["FilePath"] as! String
debugPrint("We were prompted to save something at: \(filePath)");
return filePath
}
}

Test AppleScript

tell application "ScriptableSwift" to save in "path/to/file"

Result:

"path/to/file"

Making cocoa application scriptable

Developer Library - Introduction to Cocoa Scripting Guide

Scriptable Application Example

This is easier than I thought! Some things to note:

  • You must enable your Cocoa app to receive AppleScript events with two special parameters in your Info.plist file: NSAppleScriptEnabled and OSAScriptingDefinition.
  • A special XML file called an sdef file maps your AppleScript syntax directly to an Objective C class to handle it.
  • The class must subclass NSScriptCommand and its method performDefaultImplementation, of which you can then access [self directParameter] and [self evaluatedArguments].
  • So when you send an AppleScript command, your application opens automatically (even if closed), and doesn't load multiple instances of it, which is nice. Then, if the syntax is correct in the AppleScript, it sends it to that class for handling it. The sdef file indicates which class.
  • Your command can then also send a response back, if that's what you want. Or it can choose to not do so.

1. Open your OSX Cocoa application project of which you want to receive AppleScript events.

2. Add in a commands.sdef file with the following content:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE dictionary SYSTEM "file://localhost/System/Library/DTDs/sdef.dtd">
<dictionary xmlns:xi="http://www.w3.org/2003/XInclude">
<!-- commented out next line because it has a bug ".sdef warning for argument 'FileType' of command 'save' in suite 'Standard Suite': 'saveable file format' is not a valid type name" -->
<!-- <xi:include href="file:///System/Library/ScriptingDefinitions/CocoaStandard.sdef" xpointer="xpointer(/dictionary/suite)"/> -->
<suite name="Acceptable Commands" code="SVrb" description="">
<command name="notify user with title" code="SVrbDpCm" description="">
<cocoa class="myCommand"/>
<direct-parameter description="">
<type type="text"/>
</direct-parameter>
<parameter name="subtitle" code="arg2" type="text" optional="yes"
description="">
<cocoa key="subtitle"/>
</parameter>
<parameter name="text" code="arg3" type="text" optional="yes"
description="">
<cocoa key="text"/>
</parameter>
<!-- uncomment below if you want to return a result string -->
<!-- <result type="text" description=""/> -->
</command>
</suite>
</dictionary>

3. Add in a myCommand.mm file with the following content:

#import <Cocoa/Cocoa.h>

@interface myCommand : NSScriptCommand
@end

@implementation myCommand : NSScriptCommand

- (id)performDefaultImplementation {
NSString *sResult = @"";
NSString *sTitle = [self directParameter];
NSDictionary *asArgs = [self evaluatedArguments];
NSString *sSubTitle = [asArgs objectForKey:@"subtitle"];
NSString *sText = [asArgs objectForKey:@"text"];
sResult = [sResult stringByAppendingFormat:@"TITLE=%@ SUBTITLE=%@ TEXT=%@",sTitle,sSubTitle,sText];
NSUserNotification *n = [[NSUserNotification alloc] init];
n.title = sTitle;
if (![sSubTitle isEqualToString:@""]) {
n.subtitle = sSubTitle;
}
n.informativeText = sText;
[NSUserNotificationCenter.defaultUserNotificationCenter deliverNotification:n];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]];
return sResult;
}

@end

4. Ensure the AppKit.framework is a linked binary like you normally do.

5. Edit your Info.plist file and add two parameters. (Note, below are their raw key names.)

NSAppleScriptEnabled = YES

OSAScriptingDefinition = commands.sdef

6. Compile your project. Then, right click on the product under the yellow Products folder in XCode, and choose Show in Finder.

7. Click on that app icon you see in Finder and then press your left Option key down. Then, while holding that Option key down, go to your Finder's Edit menu and choose Copy Pathname. That puts the long pathname to your application (before you install it in /Applications) on your clipboard.

8. Just for testing this, open a terminal window and then type this, replacing the path below to the path to your Cocoa application.

osascript -e 'tell app "/crazy/long/path/Example.app" to notify user with title "My Title" subtitle "my sub" text "my text"'

9. You should see your application open up with a dock icon if it's not open already, and then in the upper right you'll see a notification appear in your sidebar notifications. It will also have the icon of your GUI application if you installed one already with it. Note that if you run that command multiple times, AppleScript is smart and doesn't load your application multiple times. Note also that if you click the sidebar notification, it will open your application screen (rather than iconifying it to the dock only).

Further Info

• You can revise the code in steps 2 and 3 to do a different command and to act upon it differently.

• You can revise your sdef (see the commented line) to return a result back, should you ever want that. (Great for debugging!)

• Your sdef file can have separate commands going to separate classes. See Apple's example for how this is done.

• The sdef file has some strange syntax such as the "code" parameter. More info is available here.

• This AppleScript connection is extremely useful if you have an application that works in conjunction with a LaunchDaemon but the LaunchDaemon needs to send a sidebar notification. Sure, there's code to make that happen, and there's a trick even to make that LaunchDaemon icon show your application icon instead of a Terminal icon, but the one thing it can't do is make it such that when you click that sidebar notification it opens the GUI application. Instead, when one clicks that sidebar notification, it opens the LaunchDaemon, which is an undesired result. So, instead, you can use this AppleScript technique to have your LaunchDaemon wake up and notify its GUI application companion to send the notification. That way, when one clicks the sidebar notification, it opens the GUI application, not the LaunchDaemon.

• It's trivial to make another application send some AppleScript:

NSString *sScript = @"tell app \"/Applications/Calculator.app\" to activate";
NSAppleScript *oScript = [[NSAppleScript alloc] initWithSource:sScript];
NSDictionary *errorDict;
NSAppleEventDescriptor *result = [oScript executeAndReturnError:&errorDict];

How to run a shell script from cocoa application using Swift?

Pre Swift 3

You can use NSTask (API reference here) for this.

A NSTask takes (among other things) a launchPath which points to your script. It can also take an array of arguments and when you are ready to launch your task, you call launch() on it.

So...something along the lines of:

var task = NSTask()
task.launchPath = "path to your script"
task.launch()

Post Swift 3

As @teo-sartory points out in his comment below NSTask is now Process, documented here

The naming and way you call it has changed a bit as well, here is an example of how to use Process to call ls

let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/ls")
try? process.run()

If you want better access to/more control over the output from your invocation, you can attach a Pipe (documented here).

Here is a simple example of how to use that:

let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/ls")

// attach pipe to std out, you can also attach to std err and std in
let outputPipe = Pipe()
process.standardOutput = outputPipe

// away we go!
try? process.run()

//read contents as data and convert to a string
let output = outputPipe.fileHandleForReading.readDataToEndOfFile()
let str = String(decoding: output, as: UTF8.self)
print(str)

Would you like to know more

You can have a look at:

  • an example gist here
  • This answer (in Objective C)
  • This tutorial from Ray Wenderlich
  • This article by Paul Hudson

Hope that helps you.

Issues with making Cocoa app AppleScript-scriptable

The fact that you're not seeing the proper class name such as animals catalogue but instead see the «class» code is a good indicator that your -objectSpecifier function returns the wrong specifier.

There is an important rule that one need to follow with -objectSpecifier:

The key passed to -initWithContainerClassDescription: needs to be the same as the one that's entered for the property definition for that class under <cocoa key>. In your example, they do not match, however: The property accessor is called animalsCatalogue but the key you use in -objectSpecifier is allCatalogues. They need to match.

Also, as the Apple docs say, the containerSpecifier may only be nil if the target is the application, but not for any other scriptable objects. The Cocoa Scripting engine won't probably report this back as an error if you pass nil for a non-application object, but the results will be similar to passing a wrong key.

Build a Mac OS X Cocoa app for running a shell script with arguments

Just add the arguments to your arguments array:

    let arguments = ["/path/to/script", sourcePath, destinationPath]


Related Topics



Leave a reply



Submit