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
andOSAScriptingDefinition
. - 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 methodperformDefaultImplementation
, 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
Cannot Assign to Property: 'Self' Is Immutable, I Know How to Fix But Needs Understanding
Using Just with Flatmap Produce Failure Mismatch. Combine
Select Next Nstextfield with Tab Key in Swift
Ios-Charts Library: X-Axis Labels Without Backing Data Not Showing
How to Handle Touch Gestures in Swiftui in Swift Uikit Map Component
How to Check If Annotation Is Clustered (Mkmarkerannotationview and Cluster)
How to Draw a Scnnode Always in Front of Others
Terminate Subprocesses of MACos Command Line Tool in Swift
How to Play Sound with Avaudiopcmbuffer
Cut a Circle Out of a Uiview Using Mask
Sliding One Swiftui View Out from Underneath Another
Passing Data Between Two Nsoperations
How to Position Banner Ads Over Uitabbar
Kvo with Shared Nsuserdefaults in Swift
Swift Arc4Random_Uniform(Max) in Linux